djangosucks · janoderso_ ·

Django sucks.

django is the framework equivalent of your dad's 2007 toyota camry and i'm tired of pretending it isn't

ok so i need to vent. i've been writing django for like 6 years now and i finally hit my breaking point yesterday at 2am when i was debugging a migration that django generated itself and then refused to apply because of a circular dependency that it introduced. so here we go. strap in. this is going to be long because i have grievances and a six pack and nothing better to do on a saturday night.

the orm is gaslighting me and i'm tired

you ever write a queryset and django just goes "haha sure 👍" and then in production it issues 47,000 sql queries to render a list page? and you go look at the code and it's literally just {% for thing in things %}{{ thing.related_thing.name }}{% endfor %} and django is like "well yeah, you didn't say select_related, what was i supposed to do, be smart about it?"

YES. YES THAT IS EXACTLY WHAT YOU WERE SUPPOSED TO DO.

the django orm has two modes:

  1. shockingly elegant

  2. eating your database alive while making eye contact with you

and there is no in between. and the worst part is the docs will tell you "just use select_related!" like that's a solution and not an admission that the default behavior is a footgun aimed directly at your femoral artery. every django dev i know has the same ptsd. every single one. we all know what prefetch_related is. we all know it because we all had a Bad Day.

also Q objects. why. why is it Q(name="bob") | Q(name="alice"). why is this a thing. why does my filter look like i'm summoning a demon. sqlalchemy just lets you write or_(Thing.name == "bob", Thing.name == "alice") like a normal person. but no, django wanted to be Quirky™ so now we have Q. and F. and probably some others i'm forgetting. the entire alphabet eventually.

migrations: a story of betrayal

let me set the scene. you and your coworker both add a field to the same model on different branches. you both run makemigrations. you both get a migration file. you both push.

congratulations, you now have two leaf nodes in the migration graph and django is pissed about it. so you create a merge migration, which is a migration whose entire purpose is to tell django "hey it's fine, calm down, both of these happened, please continue." it does literally nothing. it is a participation trophy migration. and you have to commit it. forever. it lives in your repo. for eternity. testifying to the time two people touched the same model on the same day.

and god help you if you ever want to squash migrations. squashing migrations is one of those things django technically supports the way airlines technically support you bringing a tuba on the plane. yeah you can do it. you're not gonna enjoy it. someone is gonna cry.

meanwhile alembic is over here just diffing your schema like it's not even a thing. like it's normal. like that's what computers are FOR.

the admin is a trap

ok so the django admin. yes it's amazing. yes it's the best out-of-the-box admin interface of any framework, full stop, no contest. yes i love it.

it is also a TRAP.

here is what happens. you start a project. you go "i'll just use the admin for now, it's fast." three years later your non-technical operations team is doing their entire job in the admin. they have admin actions. they have inlines. they have custom forms. they have a get_queryset override that filters by tenant. you have written 4,000 lines of admin customization. the admin IS your internal tool now.

and then someone says "hey can we change the admin to do X" and X is something the admin was never designed to do, like, idk, have a non-tabular layout. or two-step workflows. or anything resembling a modern UX. and now you're either:

a) fighting django admin internals like you're trying to disarm a bomb in a movie b) building a whole second internal tool from scratch c) telling ops "no, we can't do that, sorry"

the admin is heroin. the admin is a honeypot. the admin is the reason your startup is still using a UI that looks like it was designed in 2009 because IT WAS.

"django is batteries included" yeah and the batteries are aa and i need a 9-volt

every time someone defends django they go "BUT IT'S BATTERIES INCLUDED!" ok cool. let's see what's in the box.

  • auth: from 2005, still uses pbkdf2 by default in some configs, the user model is famously a nightmare to customize after the fact, you WILL need a custom user model and you WILL forget to set it before your first migration and you WILL start the project over

  • forms: see above, mostly useless in modern apps

  • templates: jinja2 exists and is better in every measurable way and django's docs are like "well you can use jinja2 if you really want to 🙄"

  • admin: love it, hate it, see above

  • caching: it's fine i guess

  • sessions: works

  • middleware: a system designed in 2005 that we are all still using because changing it would break everything

the "batteries" are largely batteries from a previous decade. the reason django feels productive isn't because the batteries are good, it's because django at least picked batteries so you don't have to. flask makes you choose your own adventure. django chose for you, in 2008, and never updated the choices.

async support is a hostage situation

django added async views. cool! great! welcome to 2015!

except. EXCEPT. the orm is still sync. so you're in an async def view and you want to query the database and you have to use sync_to_async or await Model.objects.aget() (which exists for SOME methods but not others, you have to check) and the entire thing feels like django is humoring you. like a parent letting a kid "drive" by sitting on their lap and holding the wheel.

want to use an async cache backend? hope you like writing it yourself. want to use an async-native task queue? celery is sync, rq is sync, django-q is sync. there's a cottage industry of people trying to make it work and it sort of does and you can sort of build async django apps now but it feels like you're doing it on hard mode, on a framework that openly resents you for trying.

fastapi shipped async-first in 2018 and django is still going "we're getting there 🥺"

settings.py: one file to rule them all and in the darkness bind them

settings.py. one giant python file. with everything in it. database config, installed apps, middleware, template config, static files, media files, auth backends, cache config, email config, logging config (the logging config is its own special hell, a dictconfig nightmare nested seven layers deep), debug flags, secret keys, feature flags people added because where else would they put them, that one weird X_FRAME_OPTIONS = 'DENY' line nobody remembers adding...

and because it's python, people DO THINGS in it. they import stuff. they run code. they have if DEBUG: branches. they read environment variables in fourteen different ways. they have a local_settings.py that gets imported at the bottom with try: from .local_settings import * except ImportError: pass like it's 2010.

every django project's settings.py is a unique snowflake of dysfunction and you cannot understand a django project without reading it top to bottom and even then you'll miss something.

twelve-factor app? in MY django? it's more likely than you think but only because you bolted on django-environ.

class-based views are a riddle wrapped in a mystery inside a mixin

ok so function-based views: simple, clear, you read them top to bottom, you know what they do.

class-based views: please consult the classy class-based views website to figure out what your view actually does because the method resolution order goes through View -> TemplateResponseMixin -> ContextMixin -> SingleObjectMixin -> SingleObjectTemplateResponseMixin -> BaseDetailView -> DetailView and somewhere in there get_context_data is called and you need to figure out which ancestor's version is the one running.

ccbv.co.uk should not need to exist. its existence is an indictment. the fact that the entire django community relies on a third-party site to figure out what their own framework's class hierarchy does is, like, a five-alarm fire that we've all just decided to live with.

i once spent two hours figuring out where to override a method in an UpdateView because the method was defined on a mixin that was applied at a different layer than where i was looking. two hours. for an UPDATE FORM.

drf is not a solution it's a hostage negotiation

"oh you want to build an api in django? use django rest framework!"

drf is fine. drf is good, even, in places. but drf is also:

  • serializers that are basically a second forms system, with a different api, different validation flow, and different conventions

  • viewsets that are class-based views but MORE so, with even more mixins and even more magic

  • a permissions system that is its own dsl essentially

  • a router that pretends to be restful but the moment you need anything custom you're back to writing url patterns by hand

  • pagination that has three different built-in styles and you will pick the wrong one

  • a "browsable api" that everyone disables in production because it's slow and weird

and the docs! drf's docs are famously good in tone and famously bad in completeness. you will read a page, think "great, i understand ModelViewSet now," and then immediately need something not covered and end up on stack overflow reading a 2017 answer that references a version of drf two majors ago.

meanwhile fastapi is over here generating openapi schemas from type hints. just from the type hints. that you were going to write anyway. and the docs are interactive. and validation is just pydantic. and i'm sitting here writing a ModelSerializer with Meta classes in 2026 like a victorian peasant.

the community vibe is. a thing.

look. i love python. i love the python community. but the django community has a very specific energy that is hard to describe. it is the energy of a long-running irc channel that has been arguing about the same three things for fifteen years.

bring up async? "django doesn't need to be fast, that's not what it's for." (then what IS it for, brenda)

bring up the orm's n+1 issues? "just use select_related, this is a developer education issue." (it's a footgun, debra)

bring up that the templates are slow and limited? "use jinja2." (why isn't it the default, gerald)

bring up htmx? you'll get either "FINALLY someone gets it, this is the future" or "this is just web 1.0 with extra steps" and there is no middle ground.

every django thread on r/django or r/python eventually devolves into the same handful of takes. it is the framework equivalent of a thanksgiving dinner where everyone has the same five arguments every year and nobody actually changes their mind.

but also.

ok i need to come clean. despite everything i just wrote. i'm starting a new project next week. and i'm probably going to use django.

because here's the thing. for all its sins, django works. it's been around for 20 years. every problem you'll hit, someone hit in 2014 and wrote a stack overflow answer. every weird edge case has a package. every deployment platform supports it. it has the admin (yes, the trap, but god the trap is convenient on day 1). it has stable apis. it doesn't break between versions in the way that, say, every javascript framework breaks every six weeks.

django is the framework you marry, not the framework you fall in love with. it's reliable. it's boring. it's there. when i need to ship a CRUD app for a small business, i don't want excitement. i want the camry. i want it to start in the morning and get me to work and not need a podcast about its philosophy of routing.

so yeah. django sucks.

it sucks in the specific way that things you can't quit suck. it sucks the way that family sucks. you can complain about it forever and you'll still show up for thanksgiving.

i hate it here. but also nowhere else feels like home.

flame on.

EDIT: rip my inbox

EDIT 2: to everyone saying "skill issue" — yes. and?

EDIT 3: no i will not try ruby on rails. i have looked into the abyss before.

EDIT 4: the fastapi people in the comments are exactly as smug as i feared

2

2 Comments

Log in to comment.

definitly_not_the_entire_bee_movie_script_who_would_ever_do_anything_like_that_ever_wtf ·
vallah so ein schwanz framework
1
definitly_not_the_entire_bee_movie_script_who_would_ever_do_anything_like_that_ever_wtf · 1 reply
janoderso_ ·
frfr
1
janoderso_ · 0 replies