How Pyrefly Handles Django ORM Models, QuerySets, and View Decorators

Pyrefly handles Django ORM models, QuerySets, and view decorators by loading official type stubs (django-stubs) and applying its generic type solver to propagate precise types through the AST, without any framework-specific hardcoding.

Pyrefly is a performant Python type checker developed by Meta (facebook/pyrefly) that analyzes code by consuming type stub files rather than embedding framework-specific logic. When working with Pyrefly Django ORM patterns, the tool leverages the django-stubs package to understand generic QuerySet types and decorator signatures, treating them as standard parametric types within its solver.

How Pyrefly Types Django ORM QuerySets

Pyrefly does not contain special-purpose Django code for the ORM. Instead, it relies on the django-stubs package where Model is defined as a generic base class and QuerySet is declared as class QuerySet[ModelType, Any]: ….

Generic Model and QuerySet Definitions

When a Python file imports django.db.models, Pyrefly loads the corresponding .pyi stub files from django-stubs. The stubs define Model as a generic base class, allowing Pyrefly to record concrete model definitions as specific instantiations of that generic.

When you subclass a model:

class MyModel(models.Model):
    ...

Pyrefly records that MyModel is a concrete Model type. The generic QuerySet is then instantiated as models.QuerySet[MyModel, MyModel] when accessed through that model class.

Type Propagation Through Inheritance

The core type solver (pyrefly_types crate) treats generic classes like QuerySet as parametric types and propagates them through attribute accesses and method calls. This respects the variance rules defined in the type system, ensuring that MyModel.objects.all() returns the correctly parameterized QuerySet type rather than a generic Any.

Test Validation with django_testcase!

The inference logic is exercised in the test suite via the django_testcase! macro defined in pyrefly/lib/test/django/util.rs. This macro creates a temporary Django environment, runs a Python script, and checks inferred types against expectations using typing_extensions.assert_type.

In pyrefly/lib/test/django/view.rs, the test suite verifies that for a ListView[MyModel] instance:

from django.db import models
from django.views.generic.list import ListView
from typing_extensions import assert_type

class MyModel(models.Model):
    ...

class MyListView(ListView[MyModel]):
    ...

list_view = MyListView()

# Pyrefly infers:

assert_type(list_view.queryset,
            models.QuerySet[MyModel, MyModel] | None)   # ✅

assert_type(list_view.get_context_object_name(models.QuerySet[MyModel]()),
            str)                                          # ✅

The test confirms that detail_view.queryset and list_view.queryset both infer as models.QuerySet[MyModel, MyModel] | None.

Handling Django View Decorators

Pyrefly handles Django view decorators by resolving their types from stubs and folding them into the wrapped function's type signature.

Callable Signature Preservation

Decorators such as @user_passes_test are defined in the Django stubs as callables that accept a function and return a new function with the same signature (plus optional extra parameters). Pyrefly parses the decorator expression, resolves the decorator's type from the stub, and preserves the original function's signature through the decoration process.

Decorator Resolution in the Type Solver

When a decorator's stub is typed as Callable[[Callable[..., R]], Callable[..., R]], the solver substitutes the inner callable's signature into the outer type. This yields a function type identical to the original, ensuring that decorated views maintain their type safety without requiring explicit annotations.

The test in pyrefly/lib/test/django/view.rs verifies this behavior:

from django.contrib.auth.decorators import user_passes_test
from django.http import HttpRequest, HttpResponse

@user_passes_test(lambda u: u.is_active)
def my_view(request: HttpRequest) -> HttpResponse:
    ...

# Pyrefly knows that `my_view` still has type

# (HttpRequest) -> HttpResponse

The Architecture Behind Django Support

The underlying mechanism supporting Django is the same for any third-party library with type stubs.

Environment Setup and Stub Loading

The django_testcase! macro creates a TestEnv that adds the path to a local copy of the Django source and the django-stubs package to the Python import search path. The pyrefly_python crate (located in crates/pyrefly_python/src/*) reads the .pyi files, builds a symbol table, and resolves generic parameters.

Core Type Solver Mechanics

The pyrefly_types crate (in crates/pyrefly_types/src/*) handles the heavy lifting. It treats generic classes like QuerySet as parametric types and propagates them through the AST. Because this logic lives in the generic type solver, Pyrefly automatically supports any library that provides proper type stubs, including Django's ORM and its class-based view decorators.

Additional QuerySet usage examples, such as author.authors.all() patterns, are tested in pyrefly/lib/test/django/many_to_many.rs.

Summary

  • Pyrefly relies on django-stubs rather than hardcoded Django logic to understand the framework's API.
  • ORM Models are handled as generic base classes, with concrete models instantiating specific QuerySet[ModelType, ModelType] types.
  • QuerySet inference is validated through the django_testcase! macro in pyrefly/lib/test/django/util.rs and test files like pyrefly/lib/test/django/view.rs.
  • View decorators preserve function signatures by resolving stub definitions as Callable transformations in the type solver.
  • The architecture uses the pyrefly_python crate for stub loading and pyrefly_types for generic type propagation, enabling automatic support for any properly stubbed library.

Frequently Asked Questions

Does Pyrefly require special configuration for Django projects?

No special configuration is required. Pyrefly automatically discovers and loads django-stubs from your Python environment. As long as the stubs are installed in the Python path, Pyrefly's frontend (pyrefly_python crate) will read the .pyi files and build the appropriate symbol tables for type checking.

How does Pyrefly handle generic QuerySet methods like filter()?

Pyrefly handles filter() and similar methods by resolving their definitions in the django-stubs .pyi files. Since QuerySet is defined as class QuerySet[ModelType, Any], methods that return Self or QuerySet[ModelType, ...] are automatically instantiated with the concrete model type. The core type solver propagates these parametric types through method chains, ensuring that MyModel.objects.filter(...) returns QuerySet[MyModel, MyModel].

Can Pyrefly infer types for custom Django decorators?

Yes. Pyrefly can infer types for custom decorators as long as they are properly typed in stubs or inline annotations. If a decorator is typed as Callable[[Callable[..., R]], Callable[..., R]] (or similar), the type solver will substitute the wrapped function's signature into the decorator's return type, preserving the original callable signature through the decoration process.

What version of django-stubs does Pyrefly use?

Pyrefly does not bundle a specific version of django-stubs. It uses whichever version is available in the Python environment configured for your project. The test suite in pyrefly/lib/test/django/ uses a local copy of the stubs to ensure consistent test results, but for production code, Pyrefly loads the stubs from your configured Python path.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →