Custom QuerySet classes and chaining
coreintermediateSubclassing models.QuerySet (not models.Manager) and adding methods that each return self.filter(...) (still a QuerySet) is what makes Order.objects.completed().recent() possible — CHAINING two custom methods back to back. This only works if the manager's get_queryset() actually returns an instance of the CUSTOM QuerySet class — a manager still returning the plain default QuerySet has no idea the custom methods even exist, breaking the chain the moment a second custom method is called.
Think of it as
filter()/exclude()/order_by() are chainable because each one returns another QuerySet of the SAME class — a custom method (completed()) is chainable in exactly the same way, as long as it also returns a QuerySet of that same (custom) class, not a plain list or a different type. The subtle trap is that WHERE the custom class lives matters just as much as writing it: Order.objects.completed() only reaches the custom completed() method if Order.objects (the manager) hands back an instance of the custom QuerySet class in the first place — a manager whose get_queryset() still returns the plain, unmodified QuerySet has no route to those custom methods at all, since Python method lookup only finds methods that exist on the actual returned object's class.
What we're doing: Chain two custom, reusable filters together — completed() and recent() — matching the exact shape the roadmap itself calls out (Order.objects.completed().recent()).
- 2
- completed() returns self.filter(...) — a NEW OrderQuerySet instance, not a plain list — which is exactly what makes calling .recent() on the result possible.
- 8
- get_queryset() returning OrderQuerySet (not the plain default QuerySet) is the wiring step that makes Order.objects.completed() reachable at all — omitting this override means completed() simply does not exist from Order.objects's perspective.
Why this works: Order.objects.completed() returns an OrderQuerySet (because completed() calls self.filter(), inheriting the class it was called on) — and since THAT object is also an OrderQuerySet, .recent() is available on it too, letting the two custom filters compose exactly like built-in QuerySet methods do.
Writing custom QuerySet methods but forgetting to override get_queryset() on the manager
Wrong
Better
What you see: AttributeError: 'QuerySet' object has no attribute 'completed' — raised the moment the custom method is called, even though it is clearly defined on OrderQuerySet.
Why: Order.objects is a plain models.Manager instance whose get_queryset() returns a plain, un-customized QuerySet — the OrderQuerySet class exists in the file, but nothing ever tells Django's manager to actually USE it, so its methods are simply unreachable from Order.objects, regardless of how correctly they were written.
- Order.objects — OrderManager
- leads to get_queryset()
- get_queryset() — returns OrderQuerySet(...)
- leads to .completed()
- .completed() — self.filter(status=...) — still OrderQuerySet
- leads to .recent()
- .recent() — self.filter(placed_at__gte=...)
Manager method vs QuerySet method
Together
Remember: A custom QuerySet method should return self.filter(...) — another instance of the same class, keeping it chainable. The manager's get_queryset() must return an instance of that CUSTOM QuerySet class, or the custom methods are simply unreachable from Model.objects. Never return a plain list from a method meant to stay chainable.
See also: manager and queryset pairing · custom managers and get queryset · laziness chaining and caching

