Skip to Content

Type Checking with Pyright

Django projects using django-stubs and djangorestframework-stubs produce thousands of false-positive Pyright errors because the stubs don’t fully model Django’s dynamic metaclass machinery. The solution is a targeted pyrightconfig.json that suppresses known false-positive rules while keeping real errors visible.

The Problem

A fresh Django + DRF project with stubs can show 3000+ Pyright errors — almost all false positives from patterns like:

# DRF serializers — `data`, `errors`, `style` are properties in stubs class MySerializer(serializers.Serializer): data = serializers.DictField() # → "DictField not assignable to property" errors = serializers.ListField() # → "ListField not assignable to property" # Django AppConfig — `default_auto_field` typed as cached_property in stubs class MyAppConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" # → type error # get_user_model() returns a class, not a type alias User = get_user_model() def create_user(user: User): ... # → "Variable not allowed in type expression"

pyrightconfig.json

Place this file at the Django project root (next to manage.py):

{ "venvPath": ".", "venv": ".venv", "pythonVersion": "3.12", "include": ["src/your_package"], "exclude": ["@dev", "@debug", "tests"], "typeCheckingMode": "basic", "reportMissingImports": "warning", "reportMissingModuleSource": "none", "reportAttributeAccessIssue": "none", "reportCallIssue": "none", "reportOptionalMemberAccess": "none", "reportIncompatibleMethodOverride": "none", "reportIncompatibleVariableOverride": "none", "reportArgumentType": "none", "reportReturnType": "none", "reportAssignmentType": "none", "reportIndexIssue": "none", "reportOperatorIssue": "none", "reportGeneralTypeIssues": "none", "reportFunctionMemberAccess": "none", "reportOptionalSubscript": "none", "reportOptionalCall": "none", "reportInvalidTypeForm": "none", "reportUndefinedVariable": "none", "reportPossiblyUnbound": "none" }

This is the config used by django-cfg itself — brings errors from 3000+ down to 0.

Rule Reference

RuleWhy suppressed
reportAttributeAccessIssueHttpRequest.user, QuerySet methods, model fields not in stubs
reportCallIssueget_user_model()(), manager .create(), signal .send()
reportOptionalMemberAccessChained ORM calls that stubs type as Optional
reportIncompatibleMethodOverrideDRF view methods typed incompatibly in stubs
reportIncompatibleVariableOverrideAppConfig.default_auto_field, DRF Serializer.data/errors/style
reportArgumentTypeDjango field choices=, model Meta options
reportReturnTypeget_queryset(), perform_create() return type mismatches
reportAssignmentTypeDRF serializer field definitions shadowing property stubs
reportInvalidTypeFormUser = get_user_model() used as type annotation
reportUndefinedVariableForward references in manager files, conditional imports
reportPossiblyUnboundVariables bound in try/except blocks used afterward
reportIndexIssuerequest.data["key"], context["request"]
reportFunctionMemberAccessDjango signal decorator patterns

What Stays Checked

With typeCheckingMode: "basic", Pyright still checks:

  • Import resolution — missing modules reported as warnings
  • Syntax errors — always reported
  • Obvious None dereferences not covered by suppressed rules
  • reportUnsupportedDunderAll__all__ listing undefined exports

Per-File Overrides

For files where you want stricter checking despite the global config, use inline suppression selectively instead of broad # type: ignore:

# Suppress one specific call generation = await sdk_client.generations.create( prompt=request.prompt, model=model, # type: ignore[arg-type] aspect_ratio=aspect_ratio, # type: ignore[arg-type] )

This documents why the suppression exists — it’s a known SDK Literal mismatch, not a real bug.

Excluding Vendor and Dev Directories

Always exclude generated/vendor code from Pyright’s scope — their errors are not actionable:

{ "exclude": [ "src/vendors", "@dev", "@debug", "@doc-internal", "tests" ] }

mypy vs Pyright

The project uses Pyright for IDE integration (VS Code / PyCharm) and mypy for CI via quality gates. They use different stubs and different heuristics — a file clean in one may show errors in the other. Maintain both:

# IDE — real-time feedback npx pyright src/ # CI — strict enforcement mypy --strict src/

TAGS: type-checking, pyright, mypy, django-stubs, drf-stubs DEPENDS_ON: [code-style, pydantic-models]

DRF validated_data False Positives

A common Pyright false positive with DRF: after serializer.is_valid(raise_exception=True), Pyright still reports errors on validated_data access because DRF’s stubs don’t have an overload for raise_exception=True.

serializer = MySerializer(data=request.data) serializer.is_valid(raise_exception=True) # Pyright may warn on these without the fix: name = serializer.validated_data["name"] # reportOptionalSubscript value = serializer.validated_data.get("value") # reportOptionalMemberAccess

Automatic Fix (django-cfg)

django-cfg fixes this automatically — no manual configuration needed.

When django_cfg is in INSTALLED_APPS, on first manage.py startup it:

  1. Writes typings/rest_framework/serializers.pyi to your project root with proper @overload for is_valid
  2. Adds "stubPath": "typings" to your pyrightconfig.json

The stub adds:

@overload def is_valid(self, *, raise_exception: Literal[True]) -> Literal[True]: ... @overload def is_valid(self, *, raise_exception: Literal[False] = ...) -> bool: ...

After Pyright picks up the new stubPath, validated_data is typed as dict[str, Any] and all access is clean — no # type: ignore needed.

The stubs are versioned: when you upgrade django-cfg, the stubs update automatically on next startup.

Manual Workaround (without django-cfg)

If not using django-cfg, suppress at the specific lines or add "reportOptionalSubscript": "none" to pyrightconfig.json:

data: dict = serializer.validated_data # type: ignore[assignment] name = data["name"]

TAGS: type-checking, pyright, drf, validated-data, stubs

Last updated on