31 Jan 2026
Django community aggregator: Community blog posts
Django's test runner is underrated
Every podcast, blog post, Reddit thread, and every conference talk seems to agree: "just use pytest". Real Python says most developers prefer it. Brian Okken's popular book calls it "undeniably the best choice". It's treated like a rite of passage for Python developers: at some point you're supposed to graduate from the standard library to the "real" testing framework.
I never made that switch for my Django projects. And after years of building and maintaining Django applications, I still don't feel like I'm missing out.
What I actually want from tests
Before we get into frameworks, let me be clear about what I need from a test suite:
-
Readable failures. When something breaks, I want to understand why in seconds, not minutes.
-
Predictable setup. I want to know exactly what state my tests are running against.
-
Minimal magic. The less indirection between my test code and what's actually happening, the better.
-
Easy onboarding. New team members should be able to write tests on day one without learning a new paradigm.
Django's built-in test framework delivers all of this. And honestly? That's enough for most projects.
Django tests are just Python's unittest
Here's something that surprises a lot of developers: Django's test framework isn't some exotic Django-specific system. Under the hood, it's Python's standard unittest module with a thin integration layer on top.
TestCase extends unittest.TestCase. The assertEqual, assertRaises, and other assertion methods? Straight from the standard library. Test discovery, setup and teardown, skip decorators? All standard unittest behavior.
What Django adds is integration: Database setup and teardown, the HTTP client, mail outbox, settings overrides.
This means when you choose Django's test framework, you're choosing Python's defaults plus Django glue. When you choose pytest with pytest-django, you're replacing the assertion style, the runner, and the mental model, then re-adding Django integration on top.
Neither approach is wrong. But it's objectively more layers.
The self.assert* complaint
A common argument I hear against unittest-style tests is: "I can't remember all those assertion methods". But let's be honest. We're not writing tests in Notepad in 2026. Every editor has autocomplete. Type self.assert and pick from the list.
And in practice, how many assertion methods do you actually use? In my tests, it's mostly assertEqual and assertRaises. Maybe assertTrue, assertFalse, and assertIn once in a while. That's not a cognitive burden.
Here's the same test in both styles:
# Django / unittest
self.assertEqual(total, 42)
with self.assertRaises(ValidationError):
obj.full_clean()
# pytest
assert total == 42
with pytest.raises(ValidationError):
obj.full_clean()
Yes, pytest's assert is shorter. It's a bit easier on the eyes. And I'll be honest: pytest's failure messages are better too. When an assertion fails, pytest shows you exactly what values differed with nice diffs. That's genuinely useful.
But here's what makes that work: pytest rewrites your code. It hooks into Python's AST and transforms your test files before they run so it can produce those detailed failure messages from plain assert statements. That's not necessarily bad - it's been battle-tested for over a decade. But it is a layer of transformation between what you write and what executes, and I prefer to avoid magic when I can.
For me, unittest's failure messages are good enough. When assertEqual fails, it tells me what it expected and what it got. That's usually all I need. Better failure messages are nice, but they're not worth adding dependencies and an abstraction layer for.
The missing piece: parametrized tests
If there's one pytest feature people genuinely miss when using Django's test framework, it's parametrization. Writing the same test multiple times with different inputs feels wasteful.
But you really don't need to switch to pytest just for that. The parameterized package solves this cleanly:
from django.test import SimpleTestCase
from parameterized import parameterized
class SlugifyTests(SimpleTestCase):
@parameterized.expand([
("Hello world", "hello-world"),
("Django's test runner", "djangos-test-runner"),
(" trim ", "trim"),
])
def test_slugify(self, input_text, expected):
self.assertEqual(slugify(input_text), expected)
Compare that to pytest:
import pytest
@pytest.mark.parametrize("input_text,expected", [
("Hello world", "hello-world"),
("Django's test runner", "djangos-test-runner"),
(" trim ", "trim"),
])
def test_slugify(input_text, expected):
assert slugify(input_text) == expected
Both are readable. Both work well. The difference is that parameterized is a tiny, focused library that does one thing. It doesn't replace your test runner, introduce a new fixture system, or bring an ecosystem of plugins. It's a decorator, not a paradigm shift.
Once I added parameterized, I realized pytest no longer solved a problem I actually had.
Side by side: common test patterns
Let's look at how typical Django tests compare to pytest's approach.
Database tests
# Django
from django.test import TestCase
from myapp.models import Article
class ArticleTests(TestCase):
def test_article_str(self):
article = Article.objects.create(title="Hello")
self.assertEqual(str(article), "Hello")
# pytest + pytest-django
import pytest
from myapp.models import Article
@pytest.mark.django_db
def test_article_str():
article = Article.objects.create(title="Hello")
assert str(article) == "Hello"
With Django, database access simply works. TestCase wraps every test in a transaction and rolls it back afterward, giving you a clean slate without extra decorators. pytest-django takes the opposite approach: database access is opt-in. Different philosophies, but I find theirs annoying since most of my tests touch the database anyway, so I'd end up with @pytest.mark.django_db on almost every test.
View tests
# Django
from django.test import TestCase
from django.urls import reverse
class ViewTests(TestCase):
def test_home_page(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
# pytest + pytest-django
from django.urls import reverse
def test_home_page(client):
response = client.get(reverse("home"))
assert response.status_code == 200
In Django, self.client is right there on the test class. If you want to know where it comes from, follow the inheritance tree to TestCase. In pytest, client appears because you named your parameter client. That's how fixtures work: injection happens by naming convention. If you didn't know that, the code would be puzzling. And if you want to find where a fixture is defined, you might be hunting through conftest.py files across multiple directory levels.
What about fixtures?
Pytest's fixture system is the other big feature people bring up. Fixtures compose, they handle setup and teardown automatically, and they can be scoped to function, class, module, or session.
But the mechanism is implicit. You've already seen the implicit injection in the view test example: name a parameter client and it appears, add db to your function signature and you get database access. Powerful, but also magic you need to learn.
For most Django tests, you need some objects in the database before your test runs. Django gives you two ways to do this:
setUp()runs before each test methodsetUpTestData()runs once per test class, which is faster for read-only data
class ArticleTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.author = User.objects.create(username="kevin")
def test_article_creation(self):
article = Article.objects.create(title="Hello", author=self.author)
self.assertEqual(article.author.username, "kevin")
If you need more sophisticated object creation, factory-boy works great with either framework.
The fixture system solves a real problem - complex cross-cutting setup that needs to be shared and composed. My projects just haven't needed that level of sophistication. And I'd rather not add the indirection until I do.
The hidden cost of flexibility
Pytest's flexibility is a feature. It's also a liability.
In small projects, pytest feels lightweight. But as projects grow, that flexibility can accumulate into complexity. Your conftest.py starts small, then grows into its own mini-framework. You add pytest-xdist for parallel tests (Django has --parallel built-in). You write custom fixtures for DRF's APIClient (Django's APITestCase just works). You add a plugin for coverage, another for benchmarking. Each one makes sense in isolation.
Then a test fails in CI but not locally, and you're debugging the interaction between three plugins and a fixture that depends on two other fixtures.
Django's test framework doesn't have this problem because it doesn't have this flexibility. There's one way to set up test data. There's one test client. There's one way to run tests in parallel. Boring, but predictable.
When I'm debugging a test failure, I want to debug my code, not my test infrastructure.
When I would recommend pytest
I'm not anti-pytest. If your team already has deep pytest expertise and established patterns, switching to Django's runner would be a net negative. Switching costs are real. If I join a project that uses pytest? I use pytest. This is a preference for new projects, not a religion.
It's also worth noting that pytest can run unittest-style tests without modification. You don't have to rewrite everything if you want to try it. That's a genuinely nice feature.
But if you're starting fresh, or you're the one making the decision? Make it a conscious choice. "Everyone uses pytest" can be a valid consideration, but it shouldn't be the whole argument.
My rule of thumb
Start with Django's test runner. It's boring, it's stable, and it works.
Add parameterized when you need parametrized tests.
Switch to pytest only when you can name the specific problem Django's framework can't solve. Not because a podcast told you to, but because you've hit an actual wall.
I've been building Django applications for a long time. I've tried both approaches. And I keep choosing boring.
Boring is a feature in test infrastructure.
31 Jan 2026 2:21am GMT
30 Jan 2026
Django community aggregator: Community blog posts
Django News - Python Developers Survey 2026 - Jan 30th 2026
News
Python Developers Survey 2026
This is the ninth iteration of the official Python Developers Survey. It is run by the PSF (Python Software Foundation) to highlight the current state of the Python ecosystem and help with future goals.
Note that the official Django Developers Survey is currently being finalized and will come out hopefully in March or April.
The French government is building an entire productivity ecosystem using Django
In a general push for removing Microsoft, Google and any US or other non-EU dependency, the French government has been rapidly creating an open source set of productivity tools called "LaSuite", in collaboration with the Netherlands & Germany.
Django Packages : 🧑🎨 A Fresh, Mobile-Friendly Look with Tailwind CSS
As we announced last week, Django Packages released a new design, and Maksudul Haque, who led the effort, wrote about the changes.
Python Software Foundation
Dispatch from PyPI Land: A Year (and a Half!) as the Inaugural PyPI Support Specialist
A look back on the first year and a half as the inaugural PyPI Support Specialist.
Django Fellow Reports
Fellows Report - Natalia
By far, the bulk of my week went into integrating the checklist-generator into djangoproject.com, which required a fair amount of coordination and follow-through. Alongside that, security work ramped up again, with a noticeable increase in incoming reports that needed timely triage and prioritization. Everything else this week was largely in support of keeping those two tracks moving forward.
Fellows Report - Jacob
Engaged in a fair number of security reports this week. Release date and number of issues for 6.0.2 to be finalized and publicized tomorrow.
Wagtail CMS News
Wagtail's new Security Announcements Channel
Wagtail now publishes security release notifications via a dedicated GitHub Security Announcements discussion category, with early alerts, RSS feed, and advisory links.
40% smaller images, same quality
Wagtail 7.3 ships with smarter image compression defaults that deliver roughly 40% smaller images with no visible quality loss, improving page speed, SEO, and reducing energy use out of the box.
Updates to Django
Today, "Updates to Django" is presented by Hwayoung from Djangonaut Space! 🚀
Last week we had 11 pull requests merged into Django by 7 different contributors - including 2 first-time contributors! Congratulations to Sean Helvey🚀 and James Fysh for having their first commits merged into Django - welcome on board!
- Added support for rendering named groups of choices using
<optgroup>elements in admin select widgets (#13883). - Dropped support for MariaDB 10.6-10.10 (#36812).
Django Newsletter
Sponsored Link 1
Sponsor Django News
Reach 4,300 highly engaged Django developers!
Articles
Django: profile memory usage with Memray
Use Memray to profile Django startup, identify heavy imports like numpy, and reduce memory by deferring, lazy importing, or replacing dependencies.
Some notes on starting to use Django
Julia Evans explains why Django is well-suited to small projects, praising its explicit structure, built-in admin, ORM, automatic migrations, and batteries-included features.
Quirks in Django's template language part 3
Lily explores Django template edge cases: now tag format handling, numeric literal parsing, and the lorem tag with negative counts, proposing stricter validation and support for format variables.
Testing: exceptions and caches
Nicer ways to test exceptions and to test cached function results.
I run a server farm in my closet (and you can too!)
One woman's quest to answer the question: does JIT go brrr?
Speeding up Pillow's open and save
Not strictly Django but from Python 3.15 release manager Hugo playing around with Tachyon, the new "high-frequency statistical sampling profiler" coming in Python 3.15.
Events
DjangoCon Europe CFP Closes February 8
DjangoCon Europe 2026 opens CFP for April 15 to 19 in Athens; submit technical and community-focused Django and Python talks by February 8, 2026.
Opportunity Grants Application for DjangoCon US 2026
Opportunity Grants Application for DjangoCon US 2026 is open through March 16, 2026, at 11:00 am Central Daylight Time (America/Chicago). Decision notifications will be sent out by July 1, 2026.
Videos
django-bolt - Rust-powered API Framework for Django
From the BugBytes channel, an 18-minute look at django-bolt including how and why you might use it.
Podcasts
Django Chat #194: Inverting the Testing Pyramid - Brian Okken
Brian is a software engineer, podcaster, and author. We discuss recent tooling changes in Python, using AI effectively, inverting the traditional testing pyramid, and more.
Django Job Board
Two new Django roles this week, ranging from hands-on backend development to senior-level leadership, building scalable web applications.
Backend Software Developer at Chartwell Resource Group Ltd. 🆕
Senior Django Developer at SKYCATCHFIRE
Django Newsletter
Django Codebase
Django Features
This was first introduced last year, but it's worth bringing renewed attention to this: a new feature proposals for Django and the third-party ecosystem.
Projects
FarhanAliRaza/django-rapid
Msgspec based serialization for Django.
adamghill/dj-toml-settings
Load Django settings from a TOML file.
This RSS feed is published on https://django-news.com/. You can also subscribe via email.
30 Jan 2026 6:00pm GMT
Customizing error code for Cloudflare mTLS cert check
Summary
The Bitwarden mobile app wipes its local cache when receiving an HTTP 403 error. By default, a WAF rule in a Cloudflare free account can only return a 403. This guide shows how to use Cloudflare Workers to validate mTLS certificates and return an HTTP 404 instead.
Background
For accessing internal services remotely, I have been a big fan of Cloudflare Tunnels. This system provides an easy mechanism to provide access without opening up firewall ports, and the ability to take advantage of Cloudflare security controls like their Web Application Firewall (WAF) and Zero Trust access control.
Previous authorization model
Up until recently, I have secured my internal applications with a simple OAuth identity validation through Zero Trust. Depending on the identity provider used, this can provide a significant amount of protection, but it can cause issues with non-web based applications like mobile apps.
One simple alternative I've used is Service Tokens, which can be provided via custom HTTP headers - if the app I'm trying to use supports that type of customization. However, this is quite rare, so I started looking for a more universal approach.
mTLS authorization
Historically, during client/server communication, the focus has been on ensuring the server endpoint is trustworthy, and where SSL/TLS is a standard requirement. There are many use cases where the server may need to assess the trustworthiness of the device communicating with it. For a very sensitive system like a password manager, validating the device in addition to the user credentials provides an extra layer of security.
Mutual TLS (mTLS) is a process that provides the missing validation, by having the client provide its certificate to the server (after the client has validated the server's certificate).
Cloudflare mTLS support on free plans
I am currently using Cloudflare's free plan, as the pricing for their Pro and Business plans is quite lofty for a single household use case.
With the free plan, you can create certificates via the Cloudflare dashboard, which ends up creating a certificate signed by a Cloudflare-managed, account-level root CA. Then, the client certificate validation can be referenced in a WAF rule.
However, there are some important limitations here:
- You cannot bring your own CA, which means the certificates cannot be used in Zero Trust access controls.
- You cannot get the cert for your individual Cloudflare-managed CA, which means you cannot bundle it into the client certificate, which is a requirement to use it some situations (like Chrome on Android).
Another limitation, not strictly related to mTLS certs, is that the Cloudflare WAF rules will always return HTTP 403 when they block traffic. Customizing the response, including the response code, is limited to Pro plans (and above).
Need for customizing the response code
I have a password manager application (Bitwarden) that caches passwords locally, which is a helpful feature when I don't have an Internet connection. However, this app will always try to sync passwords if it can. As a security mechanism, if it is able to reach the server (Vaultwarden, in my case), and the server returns a 401 or 403 response code, the app will immediately clear the local cache.
I encountered this scenario when testing my mTLS configuration. I immediately thought of future situations where this could cause significant distress - let's say I'm outside my network and need access to a password. Something has happened on my device: maybe the mTLS cert was accidentally uninstalled, or maybe it expired and I haven't realized it yet. I try to access the password, everything vanishes, and I'm stuck.
I wanted to prevent this from being a possibility, which is why I wanted to alter the response code from 403 to 404. As mentioned in the previous section, this isn't possible on the free plan.
Enter Cloudflare Workers
However, there is another option within reach of the free plan, which is Cloudflare Workers. Workers allow you to deploy some custom code that can be mapped to a specific URL path. The free plan currently allows up to 100,000 Workers requests per day, which is plenty for household use.
To get this setup, I did the following:
Create and configure the mTLS certificate
Create an mTLS certificate and enable it for the targeted domain names.
Remove any WAF rules
I first made sure any WAF rules I had did not apply to the hostname in question. In the request lifecycle, these will be executed early on.
Add a Zero Trust bypass rule (optional)
Zero Trust access rules also run prior to Workers, so you need to make sure this won't block access to your application.
If you only have Zero Trust applications set up for specific subdomains, you may not need to do anything here. I happen to have a wildcard application setup, so that anything in my domain would route to the Zero Trust handling by default. This means I needed to specify an exception just for the subdomain that I wanted to protect with an mTLS cert.
To do this, I created a "bypass all rule": 
Create the worker
Create a new worker, using the provided "Hello World" template. Replace the code with the following:
export default {
async fetch(request, env, ctx) {
// Get the certificate status from the Cloudflare edge
const clientTrust = request.cf?.tlsClientAuth;
// Check that the mTLS has been presented and verified by Cloudflare.
if (!clientTrust
|| clientTrust.certPresented !== "1"
|| clientTrust.certVerified !== "SUCCESS") {
return new Response("Not Found", {
status: 404,
headers: { "Content-Type": "text/plain" }
});
}
// If valid, forward the fetch to the backend server.
return fetch(request);
},
};Optional: If you would normally use a WAF rule to block other types of traffic, you likely can incorporate that logic into the Worker as well. For example, you could add a regional restriction like this:
if (request.cf?.country !== "US") {
return new Response("Not Found", {
status: 404,
headers: { "Content-Type": "text/plain" }
});
}Set up route and test the new worker
After deploying the worker, you can then configure the appropriate route by following the Cloudflare documentation.
Then, try accessing the application you have protected, on devices with and without the mTLS certificate installed.
Future wishes
Ideally, I would really like to be able to target mTLS certificates as part of the Zero Trust access policies. This would allow different combinations to be created, like supporting either mTLS auth or an OAuth identity provider. Depending on the application being protected, this type of configuration may provide adequate security, while providing greater flexibility - allowing web browser access with just OAuth, but falling back to mTLS auth for devices where it is the best option.
There is rich support for mTLS auth in Zero Trust, but this support is limited to Business accounts only! At hundreds of dollars per month, that's a very high price to pay given that I don't need most of what that plan provides.
If anyone from Cloudflare is listening, it'd be great to expand the availability of this feature - potentially they could offer individual feature plans, like what is currently done for Workers. I'd happily pay a reasonable amount just to get support to bring my own root CA and use it as part of my Zero Trust access policies.
Final thoughts
Securing a server shouldn't come at the cost of usability. By moving logic from the limited WAF rules in a Cloudflare free account into a Cloudflare Worker, I believe I've managed to keep the high-security of using mTLS while smoothing out the quirks of the password manager application I'm using. It's an easy-to-configure change that avoids a potential lockout.
Are you running a similar setup? Have suggestions on other ways to leverage Cloudflare's copious free functionality? Leave a comment below or send me an email.
30 Jan 2026 11:20am GMT
