I'm working with a large, 5 year old Django codebase. My humble opinion: Django itself is actively hindering our ability to deliver new features.
Some pain points:
- Django's fat models approach is bad. By coupling model behaviour to database schema, it all but guarantees that you're gonna have a tough time modifying either.
- Django's class-based views and its admin framework strongly encourages a sort of "one-view, one-model" approach. Just about nobody has requirements that neatly fit that worldview. If you need to interact with two models in a single view, working around it is painful.
- Not Django specific, but Python is a bad choice to use at scale. The lack of a good default immutable data structure means we're stuck using mutable data structures everywhere. In a large codebase, the lack of guarantees around something as basic as the shape of a data structure is a huge problem. A notorious example is Django's HttpRequest, whose attributes may or may not be set by the middlewares.
There are more, but one thing's for sure, I probably won't be using Django for my next project.
I'm a heavy Django user, but I work on a lot of data-intensive projects. I do my data modeling in the database and expose it to Django, and it feels pretty clean to me. I use the ORM for what I would consider its true purpose: converting relations and attributes to objects and properties. I use it very lightly, otherwise, like for filtering and sorting.
* Create primary "fact" models for the things you are CRUDing.
* Process the data in the database. Write your complex operations in SQL, and expose them as views.
* Create corresponding Django models on top of the views. These are not as "fat" because they are unmanaged models that just facilitate getting processed data to the user.
If you're using a good database, you accommodate writes through the views as well. I rarely find a good reason to do this, but when it comes up, it works great. It is especially useful when dealing with legacy schemas.
It won't help you with handling code size so big you really want types, but one advice I would give is ditch class based views. I find that function views much easier to reason about, even if it leads to code duplication. The ability to read the code linearly without jumping through inheritance hierarchies makes everything better.
There are TemplateView and View that give you all the good stuff of CBV's without needing to use the more tailored versions. That way instead of needing to do
if request.method == 'GET':
something()
elif request.method == 'POST':
save()
you can just override the get and post method, and do all the init stuff in dispatch. I have found this to be a really clean way to deal with views that need to do more than what the CBV's give by default.
while django encourages Fat Models and that approach ends up working well sometimes, django itself has no such approach to models. how much logic to put in a model is left to the application developer.
i think what tends to happen, though, is that coming up with a convention different than Fat Models is difficult, especially early on, so most organizations don't bother and end up with models that are badly abstracted AND fat.
this could be seen as a failure of the django community, but it is not a shortcoming of the framework itself.
You can still opt for function-based views almost 100% of the time. Though I'm a big fan of FBVs for "You see what you get", I've started to find that writing your own, project-specific CBVs might pay off for speed + enforcing style in handling requests though.
Fat models are most definitely a pain. Our solution has been to not rely on models for almost anything except for DB interaction, and instead have wrappers that act as business functions. So instead of Post.objects.create, you just write create_post. You still get ORM goodness for the most part, but your validation/creation layer is under control so you can apply your style.
Overall, Django's defaults and even the fat model approach can work really well if you're writing a CMS, for example. And fortunately it's getting easier and easier to use only what you want.
I hear you for the mutability of request objects. Mypy has helped us out a lot, but Django is pretty resistant to those.
The problem with this sort of advice though is that everyone likes to think their project will go hyperscale, or will be prepared to if necessary. The reality is that very very few of them ever need to yet so many design with this is mind not understanding the tradeoffs and what they're losing by following this approach. (Incidentally I also attribute much of the current popularity of microservices to this idea)
The fact is that once you hit hyperscale you should almost certainly not be using the same codebase as when you were still establishing exactly what your project should do.
For most projects, the fat models approach works really very well and I don't feel is something that people should be dissuaded from. For most projects, the alternatives are far harder to get clean results from.
Not OP, but as a Python developer with interests elsewhere, I'm very productive in modern C# with ASP.NET Core MVC. (And I can write it on a Mac and deploy on Linux, too!)
I'd go for libraries instead of frameworks. Frameworks tend to force you into using a specific pattern or architecture (e.g. active records for Django), and I think the programmer should have full control over that.
Having built couple of toy programs with OCaml, I'd be really interested in building a project in a functional language.
Not close to baked yet, but look forward to Rust. Hyper just got async built in, so the entire ecosystem is going to be scalability first from the start.
There are also a lot of "Flask-like" projects in Rust. Rocket for the framework, Tera for the templates, and Diesel for the ORM.
The barrier right now is mainly upstream. Rocket is a nightly-only Rust crate right now, and a lot of the UX features these kinds of frameworks need / want are still missing from the compiler (like async / await).
Python's data structures (with few exceptions) are completely mutable, which means any function can add, modify, or even remove attributes from some object at any time.
Look at Django's request object for example. Django, by convention, adds a bunch of attributes to the request object at runtime, via middlewares. This is a problem because a dev can't reason about the shape of request object, without stepping through all the middlewares and figuring out which is doing what.
We could use immutable classes internally, but that doesn't solve the problem that external libraries (like Django!) abuse them all over the place.
I share your sentiment about python in large code bases. I used to think dynamic languages like python/ruby provide superior expressiveness and therefor should be excellent choices no matter what kind of project one is working on.
Now I believe they should only be used for small projects or quick prototypes. Anything beyond that and everything starts getting very confusing.
Some pain points:
- Django's fat models approach is bad. By coupling model behaviour to database schema, it all but guarantees that you're gonna have a tough time modifying either.
- Django's class-based views and its admin framework strongly encourages a sort of "one-view, one-model" approach. Just about nobody has requirements that neatly fit that worldview. If you need to interact with two models in a single view, working around it is painful.
- Not Django specific, but Python is a bad choice to use at scale. The lack of a good default immutable data structure means we're stuck using mutable data structures everywhere. In a large codebase, the lack of guarantees around something as basic as the shape of a data structure is a huge problem. A notorious example is Django's HttpRequest, whose attributes may or may not be set by the middlewares.
There are more, but one thing's for sure, I probably won't be using Django for my next project.