## 目的 黒魔術と言われる[[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)