Custom managers and get_queryset()
coreintermediateA custom manager subclasses models.Manager and adds reusable, TABLE-LEVEL query methods — Order.objects.completed() instead of repeating Order.objects.filter(status="COMPLETED") everywhere it's needed. Overriding get_queryset() changes the STARTING point every query on that manager begins from (e.g. always excluding soft-deleted rows). A model can have MULTIPLE managers (Book.objects, Book.dahl_objects) — each is a completely independent named entry point with its own get_queryset().
Think of it as
A manager is the object sitting at Model.objects — every query starts by asking a manager for a QuerySet. A custom manager method (like with_counts()) is a shortcut for a query shape used often enough to deserve a name, exactly like a well-named function replaces a repeated expression. Overriding get_queryset() is a stronger move — it changes the DEFAULT starting QuerySet for that manager, which is why Book.objects.all() and Book.dahl_objects.all() can return genuinely different sets of Book rows from the exact same table, depending only on which manager was used to reach them. Multiple managers on one model exist because sometimes you want SEVERAL distinct "views" into the same table available at once — one manager per meaningful default filter, each independently named and independently usable.
What we're doing: Give a Book model both an unfiltered default manager and a second, filtered manager, so the same table can be queried two different ways depending on which manager is used.
- 2
- get_queryset() is overridden here, not a new method added — this means EVERY query through dahl_objects (not just a specific method call) starts pre-filtered.
- 8
- Book.objects.all() returns every book — this manager's get_queryset() was never overridden, so it stays the plain default.
Why this works: Book.dahl_objects.filter(title__startswith="The") still only ever returns Roald Dahl books, since the filter is applied ON TOP of get_queryset()'s already-narrowed starting point — this is what makes an overridden get_queryset() different from a manager method: it applies unconditionally to every query through that manager, not just one named shortcut.
Putting row-level (single-instance) logic on a manager instead of the model itself
Wrong
Better
What you see: The manager method as written does not even make sense to call — self inside a Manager method refers to the MANAGER, not to any particular Person row, so there is no single instance for self.deleted = True to apply to.
Why: A manager operates at the TABLE level — its methods return QuerySets or aggregate results spanning potentially many rows, and have no natural concept of "the current row." A method that only makes sense for ONE specific instance (marking IT as deleted) belongs on the model class itself, as an ordinary instance method, exactly the "persistence/business behavior belongs on the model, orchestration doesn't" distinction from earlier in this topic, applied to the manager-vs-model boundary specifically.
- Book table — every row
- leads to objects
- leads to dahl_objects
- objects — plain Manager — unfiltered
- leads to .all() → every book
- dahl_objects — get_queryset() filters author="Roald Dahl"
- leads to .all() → only Dahl books
- .all() → every book
- .all() → only Dahl books
The two ways to customize a manager
Together
Remember: A manager method is a named, reusable, opt-in query shortcut; overriding get_queryset() changes the DEFAULT starting point for every query through that specific manager. A model can have multiple independently-named managers. Row-level logic (operating on one instance) belongs on the model itself, never on a manager.
See also: default vs base manager · custom queryset classes · instance methods and domain logic

