All Articles
Code Quality

Adding Ruff to a Real Django Codebase: 91 Files Reformatted, 45 Real Findings

Shahid MalikBy Shahid MalikSeptember 4, 20267 min read

Wiring up Ruff for lint and format on Tabeer.ai's Django backend — exact pyproject.toml config, what a first-time run on an unlinted production codebase actually surfaces, and why it replaces the need for Black entirely.

Adding a linter to a codebase that's never had one is always a two-part story: the config, and what it actually finds. Both matter more than the setup guides usually let on. Here's exactly what happened wiring Ruff into Tabeer.ai's Django backend for the first time.

Install and Configure

pip install ruff
# backend/pyproject.toml
[tool.ruff]
line-length = 120
target-version = "py313"
exclude = ["migrations", ".venv"]

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "DJ"]  # pycodestyle, pyflakes, isort, pyupgrade, bugbear, flake8-django
ignore = ["E501"]  # line length is the formatter's job, not lint's

[tool.ruff.lint.isort]
known-first-party = ["config", "accounts", "exams", "billing", "content", "feedback", "rewards", "workflow", "community", "blog", "scholarships"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

DJ (flake8-django) is worth calling out specifically — it's the rule set that catches Django-specific issues generic Python linters miss entirely: model field ordering conventions, missing __str__ methods, that category of thing.

The First Run

ruff check . --statistics
12  F841  unused-variable
11  I001  unsorted-imports
 7  DJ012 django-unordered-body-content-in-model
 6  F401  unused-import
 3  DJ008 django-model-without-dunder-str
 2  B017  assert-raises-exception
 2  B904  raise-without-from-inside-except
 1  B007  unused-loop-control-variable
 1  B905  zip-without-explicit-strict
Found 45 errors.

45 findings on a codebase that had simply never been linted before — not unusual, and not a sign of a badly-written codebase, just the expected result of introducing static analysis to code that grew without it.

Auto-Fix the Mechanical Stuff First

ruff check . --fix

17 of the 45 auto-fixed cleanly — import sorting and unused imports, purely mechanical, zero judgment required. Then the formatter:

ruff format .

91 files reformatted. That number looks alarming until you remember it's cosmetic — quote-style and whitespace normalization, not logic changes. The move that actually matters here: run the full test suite immediately after, before touching anything else, to confirm formatting alone didn't break anything:

python manage.py test
# Ran 659 tests in 63.5s — OK

Clean. As expected for a pure-formatting pass, but "as expected" isn't the same as "don't verify it."

The 28 That Needed a Human

Here's where a first-time lint run actually earns its keep — not the mechanical fixes, the judgment calls:

  • Unused variables that mattered anyway. ruff check --unsafe-fixes --fix --select F841 auto-fixed most of these, but critically, it converted unused_var = Model.objects.create(...) into a bare Model.objects.create(...) — dropping the pointless assignment while preserving the actual database write. Deleting the whole line instead (an easy mistake with a blunter find-and-replace approach) would have silently removed real test setup.
  • A blind exception assertion, tightened to what's actually expected: self.assertRaises(Exception) around a unique_together violation became self.assertRaises(IntegrityError) — the test now proves the specific failure mode it's meant to guard, not just "something went wrong."
  • Missing raise ... from on two deliberate error-message replacements (catching a generic exception, raising a cleaner one for the caller) — from None added explicitly, so it's clear the exception chain break is intentional, not an oversight.
  • An implicit-length-match zip() given strict=True, since the two sequences being zipped are provably the same length by construction — cheap insurance against a silent truncation bug if that invariant ever breaks later.

Two categories — Django model field/method ordering (DJ012, 7 instances) and missing __str__ methods (DJ008, 3 instances) — got explicitly deferred, added to the ignore list with a comment explaining why: real findings, but reordering class members and writing meaningful __str__ representations across production model files is a judgment-call pass that deserves its own review, not something to rush through while wiring up the linter itself.

Why Not Black Too

Ruff's formatter is a from-scratch, Black-compatible reimplementation — same output style, dramatically faster (it's written in Rust). Installing Black alongside Ruff would mean two tools competing to own the same job, for the same result, at a fraction of the speed. Ruff replaced the need for Black entirely rather than sitting next to it — more on that specifically here.

What This Actually Buys

A CI check that catches unused imports, dead variables, unsorted imports, and a handful of real correctness issues (blind exception handling, missing exception chaining) before they land in a PR — not hypothetically, demonstrated: this exact first run found and fixed 45 real issues in a codebase every one of those PRs had already been reviewed and merged without a linter catching them.

If you're introducing linting to a codebase that's never had it and want the first pass done right — auto-fix what's safe, hand-fix what needs judgment, defer what needs its own review rather than rushing it — get in touch.

Related Articles

Testing

Adding pytest to a Django Project Without Rewriting a Single Test

Tabeer.ai had 659 tests written the standard Django unittest way. Adding pytest didn't mean migrating them — pytest-django runs unittest-style TestCase classes completely unchanged. Here's the actual config and what 'zero-rewrite' really means in practice.

5 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project