How to configure Ruff for Django
Ruff’s default rule set knows nothing about Django. It will not flag CharField(null=True), but it will flag every long line in an auto-generated migration. This guide fixes that: enable the DJ rule set, give Ruff the per-file ignores Django’s generated code needs, and produce a configuration block that catches Django-specific bugs without burying them under noise.
This guide assumes a working Django project. Follow Set up a Django project with uv first if there isn’t one yet.
Add Ruff to the project
uv add --dev ruffConfirm the install:
$ uv run ruff --version
ruff 0.15.12
The exact version will vary. The configuration below was verified with ruff 0.15.12.
Enable the flake8-django (DJ) rule set
Ruff ships seven Django-specific rules under the DJ prefix, all from the flake8-django plugin. None are on by default. Add DJ to extend-select in pyproject.toml:
[tool.ruff.lint]
extend-select = [
"DJ", # flake8-django: Django-specific bugs
"E", # pycodestyle errors
"F", # Pyflakes
"W", # pycodestyle warnings
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
]Run Ruff over the project:
uv run ruff check .Each DJ rule targets a Django pattern that ordinary linters miss:
| Code | Catches |
|---|---|
| DJ001 | null=True on CharField / TextField. Django convention is an empty string default; nullable string fields force callers to handle both None and "". |
| DJ003 | locals() passed as the context dict to render(). Leaks every variable in scope to the template. |
| DJ006 | Meta.exclude on a ModelForm. Future model fields silently become form fields. Use fields instead. |
| DJ007 | Meta.fields = "__all__" on a ModelForm. Same risk as exclude, opt-in by name instead. |
| DJ008 | A models.Model subclass with no __str__ method. Django Admin and the shell render it as Question object (1). |
| DJ012 | Model body content out of Django’s prescribed order (fields → managers → Meta → __str__ → save → custom methods). |
| DJ013 | @receiver placed underneath another decorator. @receiver connects the inner function to a signal; outer decorators never run. |
For full descriptions and fix examples, see the flake8-django reference.
Skip auto-generated migrations
makemigrations writes Python files that ordinary style rules treat as broken by design: long lines and verbatim hardcoded literals. Linting them produces noise the developer cannot fix without editing generated code.
A fresh 0001_initial.py for two trivial models trips three E501 Line too long errors out of the box. Tell Ruff to leave migrations alone:
[tool.ruff.lint.per-file-ignores]
"**/migrations/*.py" = ["E501", "RUF012"]The pattern matches every migrations/ directory under every Django app, regardless of nesting. E501 covers the long-line problem; RUF012 covers the mutable-default-value warning that fires on Django’s dependencies = [] and operations = [...] class attributes if RUF rules are enabled later.
To skip migrations entirely instead of selectively suppressing rules, use extend-exclude:
[tool.ruff]
extend-exclude = ["**/migrations/"]Selective suppression is preferable: it still catches F401 (unused imports) and other real bugs Django’s generator can leave behind.
Loosen rules in tests and settings
Two more files need narrower per-file rules.
settings.py defines a long list of password validator paths and a wildcard import is occasionally needed for environment-specific overrides. Add:
[tool.ruff.lint.per-file-ignores]
"**/settings.py" = ["E501", "F403", "F405"]F403 allows from .base import * in settings/production.py-style splits; F405 allows the symbols introduced by that wildcard to be used without being explicitly imported. Drop both if the project does not use wildcard imports in settings.
Tests typically use assert statements, which the security ruleset flags as S101:
[tool.ruff.lint.per-file-ignores]
"**/tests.py" = ["S101"]
"**/test_*.py" = ["S101"]Both patterns match Django’s default tests.py and pytest-style test_*.py files. Drop the S101 entry if the project does not enable the S rule set.
Apply the complete configuration
Combine everything into a single pyproject.toml block:
[tool.ruff.lint]
extend-select = [
"DJ", # flake8-django
"E", # pycodestyle errors
"F", # Pyflakes
"W", # pycodestyle warnings
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
]
[tool.ruff.lint.per-file-ignores]
"**/migrations/*.py" = ["E501", "RUF012"]
"**/settings.py" = ["E501", "F403", "F405"]
"**/tests.py" = ["S101"]
"**/test_*.py" = ["S101"]For a more aggressive baseline, layer the recommended Ruff defaults on top by adding the rules from that guide to extend-select. The per-file ignores still apply.
Run Ruff over the project
uv run ruff check .Ruff prints each violation with file, line, and rule code. On a clean Django project, the output looks like this:
$ uv run ruff check .
DJ008 Model does not define `__str__` method
--> polls/models.py:4:7
|
4 | class Question(models.Model):
| ^^^^^^^^
5 | question_text = models.CharField(max_length=200, null=True)
6 | pub_date = models.DateTimeField('date published')
|
DJ001 Avoid using `null=True` on string-based fields such as `CharField`
--> polls/models.py:5:21
...
Two ways to address what Ruff finds:
uv run ruff check --fix .rewrites violations Ruff knows how to fix automatically, like sorting imports (I001) and removing unused imports (F401). TheDJrules do not auto-fix; they require a code change.# noqa: DJ008on a single line, or# ruff: noqa: DJ008at the top of a file, suppresses the rule narrowly when a violation is intentional.
For broader policy patterns (block-level disables, codebase-wide ignores), see the Ruff configuration reference.
Add Ruff to pre-commit
The same configuration runs from a pre-commit hook. Add the official Ruff hook to .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatThe hook reads pyproject.toml automatically, so all the DJ rules and per-file ignores apply to commits and CI runs without further configuration. Bump the rev: whenever a new Ruff release ships.