I strongly agree with this view. Some things in particular that I think become anti-patterns as your Django app grows:

1. Everything is coupled to the ORM models / QuerySet, so any part of your stack can mutate the request's query. On one hand this is great for things like dynamic APIs and adding arbitrary filter params, but it's really hard to keep your concerns cleanly separated. (You could use Django Seal to work around the QuerySet mutability issues but it requires finesse).

2. The ActiveRecord pattern of calling model.save() to write to the DB is actually really restrictive; it forces you to conflate DB logic with your business logic, where the former often more naturally spans multiple domain models (i.e. your DB session logic should be at the application layer while your business logic is at the domain layer, in DDD terminology). The NHibernate / SQLAlchemy pattern of tracking dirty writes and only making DB requests when you call Session.flush() / Session.commit() is much more flexible. An example is if you have an object with a list of other objects, you might want to write some code in a transaction like "for day in week: parent.children.add(calculate_child_for(day))". In Django you'd be saying `parent.child_set.add(...)` which immediately runs a DB query. If your logic fails on the last child, you don't need to have sent the earlier DB requests. So instead you end up having to contort your business logic to avoid DB side-effects, collecting a list of unsaved items to later save. Using dirty-tracking means you can run your algorithm in "plan mode" / no side-effects by just omitting the "Session.commit()" at the end; no spurious DB calls will be made.

3. The ActiveRecord pattern of writing "model.fieldname = foo" and having every part of the stack treat your objects like CRUD data dictionaries completely breaks encapsulation; if you're trying to write proper domain models that encapsulate your business logic, you don't want everybody to be able to poke arbitrary state. You end up having to guard against everything being in arbitrarily bad states, because every field is public. In my experience it's much easier to test and less confusing to make most class members private, and have well-defined state-transition methods. But making your model members private in Django is obnoxiously verbose and you're swimming against the current at every step; the whole system is based on the assumption that your API is CRUD; you need to say something like `_owner_name = CharField(db_column='owner_name', verbose_name='owner name', ....)` in order to avoid borking your DB schema and admin pages.

4. Django apps are a trap. The tutorials suggest you should expect to create multiple apps but this will make your codebase a pain to refactor; you can't migrate models between apps, and so you're stuck with the first app structure you go with. Just use a single app `core` and pretend that apps are not a thing, unless you are implementing a completely standalone composable app in another repository.

5. Django is in general more on the "config over convention" end of the spectrum than, say, Rails. But it still has some annoying magic like requiring all your model files to be imported in the `appname.models` module. This means you need to do wacky re-importing in `appname/models/__init__.py` if you want to avoid having all your models in one file. Why not just allow us to configure the model path(s) like we can configure template and other paths?

Having said all this, I agree with you that Django is really productive for the early phases of a project. When I started working in Flask I had to spend a few days figuring out each of a large number of things that are just batteries-included in the Django framework. So I can see the appeal, and for small side-projects I'd still probably reach for Django. But I think I'd probably start from Flask or FastAPI for new startups.