Djangoで動的に生成されるフィールドをVSCode環境で型チェックする際のポイント

VSCodeで開発しているDjangoを利用した個人プロジェクトで、動的に生成されるフィールドの型チェックがうまくいかず、試行錯誤を繰り返しました。最終的に解決策を見つけたので、その内容を紹介します。

---

## 検証に利用した環境

- ツール
  - [VSCode](https://code.visualstudio.com/) v1.97.2
  - [uv](https://docs.astral.sh/uv/) v0.6.2
- ライブラリ
  - [Django](https://docs.djangoproject.com/ja) v5.1.6
  - [Django REST framework](https://www.django-rest-framework.org/) v3.15.2
- リポジトリ
  - https://github.com/ponko2/vscode-remote-try-django

---

## 1. [Mypyプラグイン](https://github.com/typeddjango/django-stubs?tab=readme-ov-file#installation)を利用する
[django-stubs](https://github.com/typeddjango/django-stubs) と [djangorestframework-stubs](https://github.com/typeddjango/djangorestframework-stubs) を dev dependencies に追加して下記のように[Mypyプラグイン](https://github.com/typeddjango/django-stubs?tab=readme-ov-file#installation)の設定を行います。

`django_settings_module` は環境変数 `DJANGO_SETTINGS_MODULE` で設定することも可能です。

```toml:pyproject.toml
[dependency-groups]
dev = [
    # ...
    "django-stubs[compatible-mypy]>=5.1.3",
    "djangorestframework-stubs[compatible-mypy]>=3.15.3",
    # ...
]

[tool.django-stubs]
django_settings_module = "myproject.settings"

[tool.mypy]
plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"]
```

[Mypyプラグイン](https://github.com/typeddjango/django-stubs?tab=readme-ov-file#installation)の役割は下記URLの説明がわかりやすいです。

* https://github.com/typeddjango/django-stubs/issues/579#issuecomment-950200788

---

## 2. `TypedModelMeta` を継承する
dependencies に [django-stubs-ext](https://pypi.org/project/django-stubs-ext/) を追加し、 `Model` の `Meta` で `TypedModelMeta` を継承します。

```toml:pyproject.toml
[project]
dependencies = [
    # ...
    "django-stubs-ext>=5.1.2",
    # ...
]
```

```python
from django.db import models
from django_stubs_ext.db.models import TypedModelMeta

class MyModel(models.Model):
    example = models.CharField(max_length=100)

    class Meta(TypedModelMeta):
        ordering = ["example"]
        constraints = [
            models.UniqueConstraint(fields=["example"], name="unique_example"),
        ]
```

---

## 3. `python -m mypy` で [Mypy](https://www.mypy-lang.org/) を実行する
`python -m mypy` や `uv run -m mypy` で実行しないと動的に生成されるフィールドの型チェックで下記のようなエラーが発生します。

> error: "MyModel" has no attribute "id"  [attr-defined]
> error: "MyModel" has no attribute "foo_id"  [attr-defined]

なお、`pre-commit` の設定では [pre-commit/mirrors-mypy](https://github.com/pre-commit/mirrors-mypy) に `entry: python -m mypy` を指定しても良いのですが、下記のように設定すると [django-stubs](https://github.com/typeddjango/django-stubs) 等のバージョンを `additional_dependencies` で指定する必要がなくなり、依存関係の管理が楽になるかもしれません。

```yaml:.pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: mypy
        name: mypy
        entry: uv run -m mypy
        language: system
        types_or: [python, pyi]
        args: [--scripts-are-modules]
        require_serial: true
```

---

## 4. [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) を型チェックで利用しない
VSCodeのPython用 Language Server である [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) には [django-stubs](https://github.com/typeddjango/django-stubs) をフォークした [django-types](https://pypi.org/project/django-types/) が[含まれている](https://github.com/microsoft/pylance-release/issues/4597#issuecomment-1640577809)ため、動的に生成されるフィールド以外であれば型チェックが可能ですが、動的に生成されるフィールドについては[Mypyプラグイン](https://github.com/typeddjango/django-stubs?tab=readme-ov-file#installation)のような仕組みがないため今のところ型チェックを行うことができません。

VSCode上で動的に生成されるフィールドの型チェックを行うためには [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) の型チェックを無効化した後、後述する [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) を利用する必要があります。

```jsonc:.vscode/settings.json
{
  // ...
  "python.analysis.typeCheckingMode": "off",
  // ...
}
```

なお、`python.analysis.typeCheckingMode` はデフォルト値が `off` であるため、設定削除でも型チェックを無効化できます。

---

## 5. [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) を型チェックで利用する
`django-stubs[compatible-mypy]` の [Mypy](https://www.mypy-lang.org/) と [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) で利用する [Mypy](https://www.mypy-lang.org/) のバージョンをあわせるため、下記のような設定を行います。

```jsonc:.vscode/settings.json
{
  // ...
  "mypy-type-checker.importStrategy": "fromEnvironment",
  "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
  // ...
}
```

なお、上記の設定を行うと [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) は下記のように `python -m mypy` で [Mypy](https://www.mypy-lang.org/) を実行してくれるようです。

```sh
/path/to/.venv/bin/python -m mypy --no-color-output --no-error-summary --show-absolute-path --show-column-numbers --show-error-codes --no-pretty --show-error-end /path/to/foo.py
```

---

## この方法で型チェックを行う場合の懸念点
- 動的に生成されるフィールドはコード補完の候補に現れない
  - 行えるのは型チェックだけ
  - 機能リクエストはされている様子
    - https://github.com/microsoft/vscode-mypy/issues/37
    - https://github.com/microsoft/vscode-mypy/issues/306
- 環境変数 `DJANGO_SETTINGS_MODULE` を [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) で利用するのに手間がかかる
  - `.env` で設定できるように[なる予定](https://github.com/microsoft/vscode-mypy/issues/207#issuecomment-1791101647)のようだが[未対応](https://github.com/microsoft/vscode-mypy/issues/154)
- [Django REST framework](https://www.django-rest-framework.org/) の型を書くのがつらい
  - [djangorestframework-stubs](https://github.com/typeddjango/djangorestframework-stubs) のソースを読まないとエラーの原因がわからなかったりする
    - [Django](https://docs.djangoproject.com/ja) の型では今のところそんなことはない
  - [Django REST framework](https://www.django-rest-framework.org/) のかわりに [Django Ninja](https://django-ninja.dev/) と [Django Ninja Extra](https://eadwincode.github.io/django-ninja-extra/) を利用すると楽