## 目的
黒魔術と言われる[[Django]] + [[Django REST framework]]にて、[[mypy]]を使わずに型安全を実現するにはどうすればいいのかを調査する。普段利用している[[IDE]]は以下2つであり、いずれも[[mypy]]ではない認識。
1. [[Neovim]] + [[nvim-lspconfig]] + [[Pyright]]
2. [[VSCode]] + [[Pylance]]
今回は1の環境について調べる。
## 利用する[[スタブ (Python)|スタブ]]
デフォルトではどうやっても型を解決できないので[[スタブ (Python)|スタブ]]を利用する。候補は4つある。
1. **[[django-types]]**
2. [[django-stubs]]
3. [[djangorestframework-types]]
4. [[djangorestframework-stubs]]
結論だけ言うと、1のみを試す。
- **2と4は却下**
- `-stubs` 系は[[mypy]]利用を前提としているため
## ベースプロジェクト
[[django-types]]の有無でどう変わるかを確認したいので、ベースプロジェクトを作成する。
> [!fixme]
> [[GitHub]]のコミットリンクをはる
### 環境
| 対象 | バージョン |
| ------------------ | ---------- |
| [[macOS]] | 15.7.2 |
| [[Neovim]] | 0.11.5 |
| [[nvim-lspconfig]] | `92ee7d4` |
| [[Pyright]] | 1.1.407 |
## [[django-types]]なしでの問題を確認する
### バージョン
```
├── django v4.2
│ ├── asgiref v3.11.0
│ └── sqlparse v0.5.5
├── djangorestframework v3.16.1
│ └── django v4.2 (*)
└── ruff v0.14.11 (group: dev)
```
### Model.objects が解決しない
```python
from project.app.models import Animal
Animal.objects
# ^ Unknown
```
型推論ができないだけでなく、`objects` [[属性 (Python)|属性]]が解決しない。
```error
Cannot access attribute "objects" for class "type[Animal]"
Attribute "objects" is unknown [reportAttributeAccessIssue]
```
### Modelの[[属性 (Python)|属性]]が実際の値と異なる
```python
from typing import cast
from project.app.models import Animal
animal = cast(Animal, object)
animal.name
# ^ CharField
animal.owner
# ^ ForeignKey
```
`Animal` は以下のように定義されているので `CharField` や `ForeignKey` になるのは[[Python]]からすれば当然のこと。
```python
class Animal(models.Model):
name = models.CharField(max_length=100, blank=True, default="")
owner = models.ForeignKey(
User, related_name="animals", on_delete=models.CASCADE, null=True
)
```
ただ、**動作時は 異なる型 になる** ので、そのように推論されてほしい。
- `Animal.name`: str型
- `Animal.owner`: User型
### Modelの一部パラメーターがエラーになる
```python
class Animal(models.Model):
proper = models.BooleanField(default=False)
#^^^^^
```
```error
Argument of type "Literal[False]" cannot be assigned to parameter "default" of type "type[NOT_PROVIDED]" in function "__init__"
Type "Literal[False]" is not assignable to type "type[NOT_PROVIDED]" [reportArgumentType]
```
### Modelの逆参照ができない
```python
from typing import cast
from project.app.models import User
user = cast(User, object)
user.animals
# ^^^^^
```
```error
Cannot access attribute "animals" for class "User"
Attribute "animals" is unknown [reportAttributeAccessIssue]
```
### ViewSetでserializer、queryset、request.userの型が解決しない
```python
class AnimalViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def list(self, request: Request, *args, **kwargs):
serializer = self.get_serializer()
# ^ NoReturn
queryset = self.get_queryset()
# ^ Never
req_user = request.user
# ^ Unknown
```
## [[django-types]]ありで変化を確認する
一言で言うと『Modelに関する基本的な型だけ』は解決するようになった。
### インストール
```console
uv add --dev django-types
```
```
├── django v4.2
│ ├── asgiref v3.11.0
│ └── sqlparse v0.5.5
├── djangorestframework v3.16.1
│ └── django v4.2 (*)
├── django-types v0.22.0 (group: dev)
│ └── types-psycopg2 v2.9.21.20251012
└── ruff v0.14.11 (group: dev)
```
### ✅ Model.objects が解決しない
ちゃんと解決するようになった。
```python
from project.app.models import Animal
Animal.objects
# ^ BaseManager[Animal]
```
ちゃんと利用できる。
```python
animal = Animal.objects.all().first()
# ^ Animal | None
```
### ✅ Modelの[[属性 (Python)|属性]]が実際の値と異なる
ちゃんと解決するようになった。
```python
from typing import cast
from project.app.models import Animal
animal = cast(Animal, object)
animal.name
# ^ str
animal.owner
# ^ User | None
```
### ✅ Modelの一部パラメーターがエラーになる
エラーが出なくなった。
```python
class Animal(models.Model):
proper = models.BooleanField(default=False)
```
### ❌ Modelの逆参照ができない
これは未解決。
```python
from typing import cast
from project.app.models import User
user = cast(User, object)
user.animals
# ^^^^^
```
```error
Cannot access attribute "animals" for class "User"
Attribute "animals" is unknown [reportAttributeAccessIssue]
```
### ❌ ViewSetでserializer、queryset、request.userの型が解決しない
これも未解決。
```python
class AnimalViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def list(self, request: Request, *args, **kwargs):
serializer = self.get_serializer()
# ^ NoReturn
queryset = self.get_queryset()
# ^ Never
req_user = request.user
# ^ Unknown
```
## [[djangorestframework-types]]を入れる
### インストール
```console
uv add --dev djangorestframework-types
```
```
├── django v4.2
│ ├── asgiref v3.11.0
│ └── sqlparse v0.5.5
├── djangorestframework v3.16.1
│ └── django v4.2 (*)
├── django-types v0.22.0 (group: dev)
│ └── types-psycopg2 v2.9.21.20251012
├── djangorestframework-types v0.9.0 (group: dev)
└── ruff v0.14.11 (group: dev)
```
### ❌ Modelの逆参照ができない
これは未解決。
```python
from typing import cast
from project.app.models import User
user = cast(User, object)
user.animals
# ^^^^^
```
```error
Cannot access attribute "animals" for class "User"
Attribute "animals" is unknown [reportAttributeAccessIssue]
```
### ❌ ViewSetでserializer、queryset、request.userの型が解決しない
未解決だが少し具体的になった。
```python
class AnimalViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def list(self, request: Request, *args, **kwargs):
serializer = self.get_serializer()
# ^ BaseSerializer
queryset = self.get_queryset()
# ^ QuerySet[Unknown]
req_user = request.user
# ^ AbstractBaseUser | AnonymousUser
```
他にも `list` が `Response` を返却していないことなどによるエラーも拾えるようになっている。
```error
Method "list" overrides class "ListModelMixin" in an incompatible manner
Return type mismatch: base method returns type "Response", override returns type "None"
"None" is not assignable to "Response" [reportIncompatibleMethodOverride]
```
### 副作用と方針
`uuid` や `Meta` 、`Request` などエラーにになっていない箇所がエラーになる。これは実装は存在するが[[スタブファイル (Python)|スタブファイル]]や型が存在しないためと思われる。(たぶん)
ViewSetの以下2つは、[[ジェネリクス (Python)|ジェネリクス]]を使った堅牢な[[メソッド (Python)|メソッド]]を用意した方がよい。
- `self.get_serializer`
- `self.get_queryset`
## [[django-stubs-ext]]を入れる
### インストール
```console
uv add django-stubs-ext
```
```
├── django v4.2
│ ├── asgiref v3.11.0
│ └── sqlparse v0.5.5
├── django-stubs-ext v5.2.8
│ ├── django v4.2 (*)
│ └── typing-extensions v4.15.0
├── djangorestframework v3.16.1
│ └── django v4.2 (*)
├── django-types v0.22.0 (group: dev)
│ └── types-psycopg2 v2.9.21.20251012
└── ruff v0.14.11 (group: dev)
```
モンキーパッチも `settings.py` の冒頭に追加した。
```python
import django_stubs_ext
django_stubs_ext.monkeypatch()
```
### ❌ Modelの逆参照ができない
変わらず。
### ❌ ViewSetでserializer、queryset、request.userの型が解決しない
変わらず。
## [[basedpyright]]に切り替えて落とし所を見つける
[[Pylance]]の機能も入っているらしい[[basedpyright]]にする。基本的なエラーは以下を参考にして解消する。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" />
<span class="link-card-v2-site-name">Minerva</span>
</div>
<div class="link-card-v2-title">
📜2026-01-10 basedpyrightを入れてみた
</div>
<div class="link-card-v2-content">DjangoやDjango REST frameworkの型補完強化を目的に、macOSとNeovim環境へbasedpyrightを導入した結果、virtual textで型が表示され、final指定や型アノテーション必須の警告が発生したためpyproject.tomlで無効化した経緯を記録した</div>
<img class="link-card-v2-image" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/activity.webp" />
<a data-href="📜2026-01-10 basedpyrightを入れてみた" class="internal-link"></a>
</div>
%%[[📜2026-01-10 basedpyrightを入れてみた]]%%
### Metaのエラー
```python
class UserSerializer(serializers.ModelSerializer):
class Meta:
#^^^^
```
```error
"Meta" overrides symbol of same name in class "ModelSerializer"
"project.app.serializers.UserSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
Type "type[project.app.serializers.UserSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" [reportIncompatibleVariableOverride]
```
[[django-stubs-ext]]を入れて `TypedModelMeta` を使えばもしかしたら解消するかもしれないが、実害はないのでignoreにしておく。
```python
class UserSerializer(serializers.ModelSerializer):
class Meta: # pyright: ignore[reportIncompatibleVariableOverride]
```
## 参考
- [Deep Researchの結果](https://gemini.google.com/share/c71eb9b47507)