Skip to content
HN On Hacker News ↗

What I love about Django

▲ 134 points 116 comments by j4mie 2w ago HN discussion ↗

Pangram verdict · v3.3

We believe this text is mainly human-written, with some AI content.

10 %

AI likelihood · overall

Human
96% human-written 4% AI-generated
SEGMENTS · HUMAN 1 of 2
SEGMENTS · AI 0 of 2
WORD COUNT 768
PEAK AI % 66% · §2
Analyzed
Aug 6
backend: pangram/v3.3
Segments scanned
2 windows
avg 384 words each
Distribution
96 / 4%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 768 words · 2 segments analyzed

Human AI-generated
§1 Human · 11%

My goal at the onset of writing this essay was to celebrate the parts of Django that I, in Buttondown writ large, have found so useful and so enduring over the years.One of the challenges in doing this is what I would describe as a fundamental asset of Django itself: that it is just the right level of opinionated and structured such that it becomes, over time, invisible. And I found it hard at first to squint at our codebase and point to "quote-unquote" Django things, because the Django part of the application blends so smoothly into the aspects that are simply Pythonic or simply business logic.I do not look at Buttondown and see a Django app; I see a well-structured codebase with many things that have been solved by smarter people than myself. This, more than anything else, is what I love about Django.However poetic that might be, it makes for a short and boring blog post. So I put on a combination of thinking cap and x-ray goggles and really took a look at: what parts of Django brought us the most long-term leverage over the past few years?1. MiddlewaresDjango's middleware abstraction is incredibly simple, and thereby incredibly powerful. I think folks like me who really matured during the middleware-as-function versus middleware-as-class migration take for granted that, regardless of the actual Python primitive, Django middlewares are simple functions that act on the request/response lifecycle. All they need to do is adopt that protocol, and they can do whatever they want within it. It turns out this kind of request hook is extremely useful for a number of things: routing a request to the right newsletter based on its subdomain, capturing UTM and referrer attribution, setting Content-Security-Policy headers, recording pageviews, binding request context onto our structured logs, and — below — stamping the deployed build version.Here's the entirety of the one that stamps every response with the deployed git SHA, so a stale browser tab can notice 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: response = 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 one tool that I think the median Django developer should take more advantage of, it's middlewares.2. Models (and light inheritance)We shy away from polymorphic models, partially because we think they're a bit of a footgun, but more realistically because we just don't have many use cases that adapt well for them. However, every single model in Buttondown inherits from a base model. A trimmed version of it looks like 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 any tracked field that actually changed, fire its # handle_<field>_change hook and persist a transition row. ... class Meta: abstract = True ordering = ("-creation_date",) That base class is quietly doing a lot, all of it opt-in and additive:A UUID primary key and a creation_date on every table, for free.Public, type-prefixed IDs (sub_..., em_...) that the ORM decodes transparently, via a custom manager and queryset.Implicit change tracking: define handle_<field>_change and it runs whenever that field changes — no signal, no registration.Durable provenance: map a field to a transition table and every change is written as its own row.Per-field validation hooks (validate_<field>).An opt-in soft-delete manager, and hooks into our data-integrity checker system.It seems like an odd thing to surface in this post, but what was so convenient about Django's approach to object-orientation was that all of this was piecemeal. Grafting on new bits of common functionality did not require any significant amount of labor or migration or refactor. And it means being able to do things like provenance tracking for new fields is very, very simple — the model opts in with a single method, and the base class does the rest:# app/emails/models/email/model.py class Email(BaseModel): # Implicit change tracking: define handle_<field>_change and BaseModel # invokes it whenever that field actually changes. No signal, no wiring. def handle_body_change(self, **kwargs) -> None: AsynchronousAction.enqueue(sync_snippet_references, [str(self.id)]) # Durable provenance: map a field to a transition table and every change # is persisted as a row. Adding one is 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 base class or a refactor of any call site.3.

§2 Mixed · 66%

ActionsRather than let model classes accrete dozens of methods, every behavior a model can undergo lives in its own file under an actions/ folder beside that model — one verb per module, each exposing a call().