The instinct when someone says "add pytest" to a Django project is to assume it means a test
rewrite — pytest's fixture-based style is different enough from unittest.TestCase that it
looks like an either/or choice. It isn't, and Tabeer.ai's 659 existing
Django tests are the proof: added pytest, ran the exact same test files, zero lines of test
code changed.
Install
pip install pytest pytest-django
Configure Once
# backend/pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["tests.py", "test_*.py", "*_tests.py"]
Two lines. DJANGO_SETTINGS_MODULE tells pytest-django which settings module to load (same
thing manage.py already knows); python_files tells pytest which filenames actually contain
tests — matching this project's existing naming, both tests.py (the older convention in
several apps) and test_*.py (used in the newer ones).
Run It Against the Existing, Unchanged Test Suite
pytest -q
659 passed, 960 warnings in 66.93s
Same 659 tests python manage.py test already ran and passed. Same test files, same
class SomethingTests(TestCase): classes, same self.assertEqual(...) assertions — nothing
about the test code itself changed. pytest-django's actual job here is bridging pytest's
runner and reporting on top of Django's existing test infrastructure (test database creation,
transaction rollback between tests, Django's Client for request testing), not replacing it.
What You Actually Get for the Two Lines of Config
- Better failure output — pytest's assertion introspection shows exactly what differed on
a failed
assertEqual, without needing to add a custom message to every assertion. - Selective test running that's easier to type:
pytest exams/tests/test_views.py::RecommendationsViewTests::test_multi_exam_target_narrows_to_exactly_those_examsversus Django's equivalent dotted-path syntax — same capability, less friction reaching for it. - Access to pytest's plugin ecosystem without touching a single test —
pytest-xdistfor parallel test runs,pytest-covfor coverage reporting integrated into the same run, if and when either becomes worth adding. manage.py testkeeps working, unchanged — this is additive, not a replacement. Whichever runner a given CI job or a developer's habit prefers, both point at the same 659 tests and get the same result.
Why Not Migrate the Tests to pytest's Native Style
Fixture-based tests (def test_thing(client, db): instead of
class ThingTests(TestCase): def test_thing(self):) are genuinely nicer for a lot of testing
patterns — shared setup via fixtures composes better than setUp() inheritance chains once a
suite gets large. That's a real, separate decision with real cost: 659 tests' worth of rewrite,
touching every test file in the project, for a stylistic improvement rather than a capability
one. Worth doing deliberately, incrementally, on its own timeline — not bundled into "can we run
these under pytest," which was a two-line config change with zero rewrite required.
The Actual Takeaway
"Add pytest" and "migrate to pytest's native test style" are two different asks that get conflated constantly. pytest-django exists specifically so the first one doesn't require the second — verified here against a real 659-test Django suite, not a toy example. If a project already has Django tests, adding pytest is a config change measured in minutes, not a rewrite measured in days.
If you're deciding whether to add pytest to an existing Django project and want it done as the zero-risk config change it actually can be, get in touch.
By Shahid Malik