# How to Type-Check a Django Project with Pyrefly


[Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md), the handbook's recommended type checker, reads Django models without a plugin. Install [`django-stubs`](https://github.com/typeddjango/django-stubs), and Pyrefly follows those type stubs directly: no `mypy_django_plugin` equivalent to register, no settings module to point at, no runtime introspection.

This guide installs both with [uv](https://pydevtools.com/handbook/reference/uv.md), runs Pyrefly against a Django app, shows which ORM features resolve and which do not at Pyrefly 1.1.1, and covers the exclusions and suppressions that make a first run on an existing codebase manageable. It assumes a working uv-managed Django project; follow [Set up a Django project with uv](https://pydevtools.com/handbook/tutorial/set-up-a-django-project-with-uv.md) first if there isn't one.

## Install Pyrefly and django-stubs

Add the stubs and the checker as dev dependencies in one command:

```bash
uv add --dev django-stubs pyrefly
```

uv resolves a compatible set and writes them to the `dev` group in [`pyproject.toml`](https://pydevtools.com/handbook/reference/pyproject.toml.md):

```console
$ uv add --dev django-stubs pyrefly
Resolved 10 packages in 192ms
Installed 5 packages in 26ms
 + django-stubs==6.0.7
 + django-stubs-ext==6.0.7
 + pyrefly==1.1.1
 + typing-extensions==4.16.0
 ...
```

Django support is built in as of Pyrefly 0.42.0 and needs no extra. This guide was verified with `pyrefly 1.1.1`, `django-stubs 6.0.7`, and Django 6.0.7.

## Generate a Pyrefly config

Run `pyrefly init` to write a `[tool.pyrefly]` table into `pyproject.toml`:

```bash
uv run pyrefly init
```

Without a config, Pyrefly falls back to the low-noise `basic` preset, which silences the assignment and attribute checks that catch Django type bugs. `pyrefly init` writes a config that turns on the default preset so those checks run. See the [Pyrefly configuration docs](https://pyrefly.org/en/docs/configuration/) for the available presets.

Nothing Django-specific goes in the config. There is no plugin line and no settings module to declare, unlike [mypy](https://pydevtools.com/handbook/reference/mypy.md), which needs both.

## Run Pyrefly and confirm Django-aware types

These checks run against a small app with one model and one foreign key:

```python {filename="news/models.py"}
from django.db import models


class Reporter(models.Model):
    full_name = models.CharField(max_length=100)


class Article(models.Model):
    class Status(models.TextChoices):
        DRAFT = "DR", "Draft"
        PUBLISHED = "PB", "Published"

    headline = models.CharField(max_length=200)
    status = models.CharField(max_length=2, choices=Status.choices, default=Status.DRAFT)
    reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)
```

Point Pyrefly at the app. A model module with correct annotations reports no errors:

```console
$ uv run pyrefly check news/models.py
 INFO 0 errors
```

To see the Django-aware typing, add a probe with `reveal_type`:

```python {filename="news/checks.py"}
from typing import reveal_type

from news.models import Article


def probe() -> None:
    a = Article.objects.get(pk=1)
    reveal_type(a.headline)               # str
    reveal_type(a.reporter)               # Reporter
    reveal_type(a.reporter_id)            # int
    reveal_type(a.get_status_display())   # str
    reveal_type(Article.objects)          # Manager[Article]

    wrong: Article = Article.objects.filter(pk=1)  # bug: .filter() returns a QuerySet
```

Running the check prints each revealed type and flags the bug (file-location prefixes trimmed):

```console
$ uv run pyrefly check news/checks.py
 INFO revealed type: str [reveal-type]
 INFO revealed type: Reporter [reveal-type]
 INFO revealed type: int [reveal-type]
 INFO revealed type: str [reveal-type]
 INFO revealed type: Manager[Article] [reveal-type]
ERROR `QuerySet[Article, Article]` is not assignable to `Article` [bad-assignment]
 INFO 1 error
```

Pyrefly resolved the `CharField` as `str`, the `ForeignKey` as the related `Reporter`, the `reporter_id` shadow field as `int`, and `get_status_display()` as `str`. It caught the assignment of a `QuerySet` to a variable annotated as a single `Article`, a bug that ships silently when `objects` resolves to `Any`.

## What does Pyrefly resolve, and what does it skip?

Pyrefly covers a subset of Django's ORM. The parts driven by the model definition resolve; the parts Django wires up dynamically at runtime do not.

Resolved:

- Model field types, including auto-generated `id` and `pk`
- ForeignKey and ManyToManyField attributes, plus foreign-key `_id` shadow fields
- `Model.objects` as `Manager[Model]`, and `.get()`/`.filter()` return types
- `Choices`, `IntegerChoices`, and `TextChoices` enums and their `get_<field>_display()` methods

Not yet resolved:

- Custom manager and QuerySet methods. See the next section.
- Reverse relation accessors. `reporter.article_set` raises `Object of class Reporter has no attribute article_set [missing-attribute]`. A custom `related_name="articles"` has the same gap and is not a workaround; `reporter.articles` raises the same error.
- QuerySet transforms that reshape the row. `.values()` returns `QuerySet[Article, dict[str, Any]]` with untyped keys, `.annotate()` returns `QuerySet[Article, Article]` without the added field, and `.aggregate()` returns `dict[str, Any]`.

Class-based generic views are recognized as classes, but per-instance attributes such as `self.object` and `get_queryset()` results stay loosely typed, so a view-heavy codebase should not expect the same precision it gets on model fields.

### Custom managers degrade to the base manager

The most common blindside is a custom manager or QuerySet. Define `objects = ArticleQuerySet.as_manager()` and Pyrefly types the assignment against Django's base attribute:

```console
ERROR `Manager[Article]` is not assignable to attribute `objects` with type `Manager[Self@Model]` [bad-assignment]
```

`Article.objects` then reveals as the base `Manager[Article]`, and a custom method call does not resolve:

```console
$ uv run pyrefly check news/checks.py
 INFO revealed type: Manager[Article] [reveal-type]   # Article.objects
ERROR Object of class `Manager` has no attribute `published` [missing-attribute]
```

`Manager.from_queryset(ArticleQuerySet)` behaves the same way: the result types as `Manager[Article]` and the custom methods are lost. Silence these with the suppressions in the next section until Pyrefly's Django coverage grows.

## Turn Pyrefly on without drowning in errors

A first run on an existing Django project surfaces the gaps above across every app. Three config-level controls keep the signal usable.

Exclude generated code. Migrations are the loudest source of errors and rarely worth checking. Add `project-excludes` to `[tool.pyrefly]`:

```toml
[tool.pyrefly]
project-excludes = [
    "**/migrations/**",
]
```

Suppress a single line. A `# pyrefly: ignore[<error-kind>]` comment silences one error where an unsupported accessor is unavoidable:

```python
articles = reporter.articles  # pyrefly: ignore[missing-attribute]
```

A bare `# pyrefly: ignore` silences every error on the line. A suppressed run reports the count so nothing hides silently:

```console
$ uv run pyrefly check news/checks.py
 INFO 0 errors (1 suppressed)
```

Disable a rule project-wide. When reverse relations or custom managers produce too many `missing-attribute` errors to annotate line by line, turn the kind off in `[tool.pyrefly.errors]`:

```toml
[tool.pyrefly.errors]
missing-attribute = false
```

Every error kind is toggled by name this way; run `uv run pyrefly check --help` or read the [error-suppression docs](https://pyrefly.org/en/docs/error-suppressions/) for the full list.

## Run Pyrefly in CI and your editor

Once Pyrefly passes locally, check the whole project in one command so type errors block merges:

```bash
uv run pyrefly check
```

It runs alongside `uv run pytest` and `uv run ruff check .` in the same CI job. See [Setting up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) for a workflow that wires those checks into every push.

For live feedback while editing, Pyrefly ships a language server. Install the [Pyrefly VS Code extension](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly), or point any LSP-capable editor at Pyrefly's language server. The [Pyrefly reference](https://pydevtools.com/handbook/reference/pyrefly.md) covers editor setup.

## Choose between Pyrefly and mypy's django-stubs plugin

Pick based on how much of Django's dynamic surface a codebase leans on. Both read the same `django-stubs` package, but resolve Django differently.

Pyrefly follows the stubs statically. It needs no plugin, no settings module, and no `ALLOWED_HOSTS` annotation on a fresh `startproject`, and it runs 10-50x faster than mypy on large codebases (see the [Pyrefly reference](https://pydevtools.com/handbook/reference/pyrefly.md) for the benchmark). It covers the model-driven subset above.

[mypy with the django-stubs plugin](https://pydevtools.com/handbook/how-to/how-to-configure-mypy-and-django-stubs-in-a-uv-project.md) introspects Django at runtime, so it types custom managers, reverse relations like `question.choice_set`, settings access, and `.values()`/`.annotate()` results that Pyrefly leaves loose. The cost is mypy-only checking, a plugin and settings module to configure, and slower runs.

A project that stays close to model fields, foreign keys, and choices gets the faster, config-light path from Pyrefly. A project built on custom managers, reverse relations, and heavy QuerySet transforms gets more coverage from the mypy plugin today.

Django REST Framework is out of scope for both `django-stubs` and this page. DRF ships no type information; a typed DRF project adds [`djangorestframework-stubs`](https://github.com/typeddjango/djangorestframework-stubs) on top.

## Learn More

- [Pyrefly reference](https://pydevtools.com/handbook/reference/pyrefly.md)
- [How to configure mypy and django-stubs in a uv project](https://pydevtools.com/handbook/how-to/how-to-configure-mypy-and-django-stubs-in-a-uv-project.md)
- [ty vs Pyrefly: which Python type checker should you pick?](https://pydevtools.com/handbook/explanation/ty-vs-pyrefly.md)
- [How to migrate from mypy to Pyrefly](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-pyrefly.md)
- [How to configure Ruff for Django](https://pydevtools.com/handbook/how-to/how-to-configure-ruff-for-django.md)
- [Pyrefly Django documentation](https://pyrefly.org/en/docs/django/)
- [django-stubs on GitHub](https://github.com/typeddjango/django-stubs)
