What I love about Django

Aug 06, 2026 02:34 PM - 3 hours ago 3

My extremity astatine the onset of penning this effort was to observe the parts of Django that I, successful Buttondown writ large, person recovered truthful useful and truthful enduring complete the years.

One of the challenges successful doing this is what I would picture arsenic a basal plus of Django itself: that it is conscionable the correct level of opinionated and system specified that it becomes, complete time, invisible. And I recovered it difficult astatine first to squint astatine our codebase and constituent to "quote-unquote" Django things, because the Django portion of the exertion blends truthful smoothly into the aspects that are simply Pythonic aliases simply business logic.

I do not look astatine Buttondown and spot a Django app; I spot a well-structured codebase pinch galore things that person been solved by smarter group than myself. This, much than thing else, is what I emotion astir Django.

However poetic that mightiness be, it makes for a short and boring blog post. So I put connected a operation of reasoning headdress and x-ray goggles and really took a look at: what parts of Django brought america the astir semipermanent leverage complete the past fewer years?

1. Middlewares

Django's middleware abstraction is incredibly simple, and thereby incredibly powerful. I deliberation folks for illustration maine who really matured during the middleware-as-function versus middleware-as-class migration return for granted that, sloppy of the existent Python primitive, Django middlewares are elemental functions that enactment connected the request/response lifecycle. All they request to do is adopt that protocol, and they tin do immoderate they want wrong it. It turns retired this benignant of petition hook is highly useful for a number of things: routing a petition to the correct newsletter based connected its subdomain, capturing UTM and referrer attribution, mounting Content-Security-Policy headers, signaling pageviews, binding petition discourse onto our system logs, and — beneath — stamping the deployed build version.

Here's the entirety of the 1 that stamps each consequence pinch the deployed git SHA, truthful a old browser tab tin announcement a newer build has shipped:

# app/emails/middlewares/build_version.py class Middleware: def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: self.get_response = get_response def __call__(self, request: HttpRequest) -> HttpResponse: consequence = self.get_response(request) if settings.HEROKU_SLUG_COMMIT and not flag_is_active(CIRCUIT_BREAKER_FLAG): response[BUILD_VERSION_HEADER] = settings.HEROKU_SLUG_COMMIT return response

If there's 1 instrumentality that I deliberation the median Django developer should return much advantage of, it's middlewares.

2. Models (and ray inheritance)

We awkward distant from polymorphic models, partially because we deliberation they're a spot of a footgun, but much realistically because we conscionable don't person galore usage cases that accommodate good for them. However, each azygous exemplary successful Buttondown inherits from a guidelines model. A trimmed type of it looks for illustration this:

# app/utils/models.py class BaseModel(models.Model): creation_date = models.DateTimeField(auto_now_add=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) objects = TypeIDAwareManager() def save(self, *args, **kwargs): super().save(*args, **kwargs) # For immoderate tracked section that really changed, occurrence its # handle_<field>_change hook and persist a modulation row. ... class Meta: absurd = True ordering = ("-creation_date",)

That guidelines people is softly doing a lot, each of it opt-in and additive:

  • A UUID superior cardinal and a creation_date connected each table, for free.
  • Public, type-prefixed IDs (sub_..., em_...) that the ORM decodes transparently, via a civilization head and queryset.
  • Implicit alteration tracking: specify handle_<field>_change and it runs whenever that section changes — nary signal, nary registration.
  • Durable provenance: representation a section to a modulation array and each alteration is written arsenic its ain row.
  • Per-field validation hooks (validate_<field>).
  • An opt-in soft-delete manager, and hooks into our data-integrity checker system.

It seems for illustration an overseas point to aboveground successful this post, but what was truthful convenient astir Django's attack to object-orientation was that each of this was piecemeal. Grafting connected caller bits of communal functionality did not require immoderate important magnitude of labour aliases migration aliases refactor. And it intends being capable to do things for illustration provenance search for caller fields is very, very elemental — the exemplary opts successful pinch a azygous method, and the guidelines people does the rest:

# app/emails/models/email/model.py class Email(BaseModel): # Implicit alteration tracking: specify handle_<field>_change and BaseModel # invokes it whenever that section really changes. No signal, nary wiring. def handle_body_change(self, **kwargs) -> None: AsynchronousAction.enqueue(sync_snippet_references, [str(self.id)]) # Durable provenance: representation a section to a modulation array and each change # is persisted arsenic a row. Adding 1 is simply a one-line dict entry. @classmethod def tracked_field_to_transition_class(cls) -> dict[str, type[BaseTransition]]: return {"status": EmailStatusTransition}

Neither of these required a migration to the guidelines people aliases a refactor of immoderate telephone site.

3. Actions

Rather than fto exemplary classes accrete dozens of methods, each behaviour a exemplary tin acquisition lives successful its ain record nether an actions/ files beside that exemplary — 1 verb per module, each exposing a call(). Here's the full of "ban a subscriber," which itself composes different action:

# app/emails/models/subscriber/actions/ban.py from emails.models.subscriber.actions import end_premium_subscription from emails.models.subscriber.model import Subscriber def call(subscriber: Subscriber) -> None: if subscriber.subscriber_type == Subscriber.Type.PREMIUM.value: end_premium_subscription.call(str(subscriber.id)) subscriber.subscriber_type = Subscriber.Type.REMOVED.value subscriber.save(update_fields=["subscriber_type", "modification_date"])

4. Views

Our attack to views is highly doctrinaire and highly simple. A position must:

  1. live successful its ain file
  2. be function-based alternatively than a CBV
  3. expose that usability pinch the sanction of view
# app/emails/views/record_lifecycle_email_open.py def view(request: HttpRequest, compressed_id: str) -> HttpResponse: try: account_id, email_type = _decode_open_payload(compressed_id) except (UnicodeDecodeError, Base64Error, ValueError): return HttpResponse(TRANSPARENT_GIF, content_type="image/gif") with transaction.atomic(): LifecycleEmailEvent.objects.create( account_id=account_id, email_type=email_type, event_type=LifecycleEmailEvent.EventType.OPENED, timestamp=timezone.now(), metadata=_build_metadata(request), ) return HttpResponse(TRANSPARENT_GIF, content_type="image/gif")

Why beryllium truthful boring and aliases strict? Largely owed to the symptom of discourse switching. In my opinion, penning maintainable position codification is much astir avoiding nonaccomplishment than uncovering success, and nonaccomplishment tends to travel successful the forms of unnecessary indirection and deficiency of codification re-use: some things made simpler by making views arsenic "pure" (in the FP sense) arsenic possible.

5. Testing

I've written a batch connected my individual blog astir tests, having spent overmuch of my individual-contributor clip complete the past twelvemonth moving connected making the CI pipeline — for which the backend trial suite has agelong been the agelong rod — arsenic accelerated arsenic I can. We usage pytest and pytest-django and an absolute slew of pytest plugins. Notably, we don't really usage an off-the-shelf fixture generator for illustration Factory Boy aliases thing for illustration that, alternatively constructing them ourselves successful bid to eke retired much performance. A trial is simply a plain usability that takes the fixtures it needs and asserts against existent rows:

# app/emails/views/record_lifecycle_email_open--test.py # `account` is simply a hand-rolled fixture, colocated successful account/model--mock.py and # registered via pytest_plugins — nary factory_boy, nary mock.patch. def test_records_open_event(account): encoded = encode_open_payload(str(account.pk), "unconfirmed") petition = RequestFactory().get(f"/lo/{encoded}/") consequence = view(request, encoded) assert response.status_code == 200 arena = LifecycleEmailEvent.objects.get(account=account) assert event.event_type == LifecycleEmailEvent.EventType.OPENED

The things we time off out

Where Rails is famously omakase, 1 of the things I emotion astir about Django is each the things not mentioned supra — the ones we decided, for 1 logic aliases another, weren't the correct fresh for us.

Some examples:

Signals. I'm not moreover judge you tin opportunity we don't usage them, truthful overmuch arsenic we don't maltreatment them. We person precisely 1 awesome successful play, a lightweight nexus into django-allauth. We've recovered that soul usage of signals — connecting 2 bits of codification that we ourselves ain — is an anti-pattern that makes it harder to logic astir what's happening, aliases to amended things for performance's liking down the line.

Class-based views. I deliberation class-based views person immoderate merit successful immoderate contexts, but 1 of the hardest things to woody pinch erstwhile bopping astir a codebase is context-switching betwixt a function-based position and a class-based one. And immoderate flimsy marginal benefits a CBV mightiness person for 1 usage lawsuit aliases another, it pales successful comparison to being capable to beryllium very doctrinaire and standardized astir really each azygous position works.

Apps. We don't usage apps successful the accepted modular consciousness that Django suggests, for 2 main reasons. One, it's very difficult to woody pinch cross-app migrations, peculiarly squashing them. And two, it doesn't supply an evident use complete different organizational approaches — of which Django is mostly agnostic — for illustration conscionable grouping related models successful a folder. We person 2 exceptions to this rule. The first is our halfway API infrastructure, the bones of which unrecorded successful their ain app, solely because I built it that measurement earlier I had a much blase view. The 2nd is thing we deliberation we mightiness want to absurd retired into a third-party package aliases open-source, wherever an app helps front-load immoderate of the boundary-setting betwixt it and the remainder of our codebase.

Checks. I deliberation checks are really really cool, and portion of maine bemoans not utilizing them more. I've recovered that adopting weird tests is simply a simpler measurement to enforce various constraints wrong the system. It costs a spot of capacity — you tin opportunity that technically the REPL is slower — but again, it's 1 less moving part.

Forms and front-end stuff. We don't usage Django's shape abstraction whatsoever. In fact, our attack to building retired the beforehand extremity of the exertion is somewhat interesting: we thin connected much of a hydration-based pattern, successful which Django's occupation for astir authenticated views is to render a bladed ammunition and seed it pinch data, not to nutrient HTML. A azygous position backs astir each page of the app — it resolves the session, serializes the relationship (and, for a fistful of routes, a first page of the applicable resource) into json_script tags, and hands disconnected to Vue, which boots and hydrates from that payload alternatively of paying for an API round-trip connected load. Django renders the bones; the SPA does the rest.

Why did I usage Django successful the first place?

Buttondown is written successful Django for a boring but revealing reason: it's what I knew astatine the time. Back successful 2018, I was moving for a institution whose stack was Django and Vue, and I had been hired arsenic personification pinch extended amounts of Django acquisition (meaning: I knew what South was, for you chap old-timers.)

One of my longstanding philosophies has been to limit innovation tokens. I didn't want to walk clip context-switching betwixt frameworks arsenic I went from my time occupation to my broadside project. Over the intervening 8 years, I've recovered myself ruing my prime of Vue arsenic a front-end model — but I tin honestly opportunity that, moreover if it wasn't a meticulously reasoned and considered choice, I person not for 1 2nd regretted utilizing Django.

More