skip to content

Managers & Chainable Filters

Managers vs QuerySets: custom QuerySet methods exposed with as_manager() or from_queryset(), the default manager and base_manager_name. Interviewers probe chainable domain filters.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

3

In Django, why define a published() filter on a custom QuerySet exposed with as_manager() or Manager.from_queryset() instead of only on a Manager?

level: middleimportance: must knowfreq 58%

answer

  1. manager methods vanish after filter()
  2. QuerySet methods chain anywhere
  3. as_manager copies public methods
  4. from_queryset when the manager needs its own code
  5. related managers inherit them

basics

~20 s

A Manager-only method is lost once filter() returns a plain QuerySet, so it cannot be chained. Defining published() on a QuerySet subclass exposed with as_manager() or from_queryset() makes it callable on the manager and on every QuerySet.

solid answer

~30 s

If `published()` lives on a `Manager`, `Episode.objects.published()` works but `Episode.objects.filter(podcast=p).published()` raises `AttributeError`, because `filter()` returns a plain `QuerySet`. Put the method on a `QuerySet` subclass that returns `self.filter(...)`, and it chains in any order: `Episode.objects.filter(podcast=p).published().recent()`. `EpisodeQuerySet.as_manager()` builds a manager that copies the QuerySet's public methods, so `Episode.objects.published()` works too. `Manager.from_queryset(EpisodeQuerySet)` returns a manager **subclass** with the same copies, used when the manager also needs its own methods or an overridden `get_queryset()`. Private (underscore) methods are not copied unless marked `queryset_only = False`. Because reverse related managers subclass the default manager's class, `podcast.episodes.published()` also works.

code

python · 32 lines
python
from django.db import models
from django.utils import timezone


class EpisodeQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status="published", published_at__lte=timezone.now())

    def explicit(self):
        return self.filter(is_explicit=True)


class EpisodeManager(models.Manager):
    def create_draft(self, podcast, title):
        return self.create(podcast=podcast, title=title, status="draft")


class Episode(models.Model):
    podcast = models.ForeignKey("Podcast", on_delete=models.CASCADE, related_name="episodes")
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")
    published_at = models.DateTimeField(null=True, blank=True)
    is_explicit = models.BooleanField(default=False)

    objects = EpisodeManager.from_queryset(EpisodeQuerySet)()


# All of these work:
# Episode.objects.published()
# Episode.objects.filter(podcast=show).published().explicit()
# Episode.objects.create_draft(show, "Pilot")
# show.episodes.published()

go deeper

for a junior

Recall that a published() method on a QuerySet subclass, exposed with as_manager(), lets you call it both on objects and after filter().

for a middle

Explain why manager-only methods break chaining, what as_manager() and from_queryset() return, and which methods get copied.

for a senior

Design the domain vocabulary as chainable QuerySet methods, use from_queryset() when the manager needs its own code, and rely on it through related managers.

for a principal

Decide how much domain logic belongs in QuerySets versus service functions, keeping filters reusable without hiding costly queries behind friendly names.

## The problem with manager-only methods In a podcast app, "published" means `status="published"` and `published_at` in the past, and it is needed everywhere: the feed, the episode list, the sitemap, the search index. The first instinct is a custom manager: ```python class EpisodeManager(models.Manager): def published(self): return self.filter(status="published", published_at__lte=timezone.now()) ``` This works for `Episode.objects.published()`, but it breaks as soon as the call is not first in the chain: - `Episode.objects.filter(podcast=show).published()` raises `AttributeError`, because `filter()` returns a plain `QuerySet`, which knows nothing about `published()`; - a helper that receives an already filtered QuerySet cannot apply the rule; - the logic ends up duplicated as ad-hoc `filter(...)` calls. ## Put the method on a `QuerySet` A `QuerySet` subclass method returns another QuerySet of the same class, so the domain vocabulary survives every step of the chain: ```python class EpisodeQuerySet(models.QuerySet): def published(self): return self.filter(status="published", published_at__lte=timezone.now()) def explicit(self): return self.filter(is_explicit=True) ``` Now `Episode.objects.filter(podcast=show).published().explicit()` works in any order. `timezone.now()` is evaluated each time the method is called, not once at import. ## Exposing it: `as_manager()` versus `from_queryset()` | | `EpisodeQuerySet.as_manager()` | `EpisodeManager.from_queryset(EpisodeQuerySet)` | |---|---|---| | Returns | a manager **instance** | a manager **class** (a subclass of `EpisodeManager`) | | Manager-only methods | none | whatever `EpisodeManager` defines | | Overriding `get_queryset()` | not possible | possible, in `EpisodeManager` | | Typical line | `objects = EpisodeQuerySet.as_manager()` | `objects = EpisodeManager.from_queryset(EpisodeQuerySet)()` | Under the hood `as_manager()` is literally `Manager.from_queryset(cls)()`. Both copy QuerySet methods onto the manager class by these rules: 1. **Public** methods are copied. 2. **Private** methods (leading underscore) are not. 3. A method with `queryset_only = False` is always copied; with `queryset_only = True` it never is. Django marks `QuerySet.delete()` and `as_manager()` itself this way. 4. A method the manager class already has is not overwritten. Each copied method simply calls `self.get_queryset().<method>(...)`, so a `from_queryset()` manager that overrides `get_queryset()` applies its base filter first. ## It reaches related managers too Django builds a reverse related manager, such as `podcast.episodes`, as a subclass of the related model's **default manager class**. If the default manager came from `as_manager()` or `from_queryset()`, its copied methods come along: ```python show.episodes.published() # episodes of this podcast, published only ``` The same is true for many-to-many managers. This is one of the strongest arguments for the QuerySet approach: the rule is written once and is usable from the model, from any QuerySet and from relations. ## A worked podcast example For a podcast site the vocabulary usually settles into a handful of chainable methods: 1. `published()`: status is published and `published_at` is in the past. 2. `scheduled()`: published status but a future `published_at`. 3. `for_podcast(podcast)`: episodes of one show. 4. `with_play_counts()`: an `annotate()` adding listen counts for the list page. Views then read like the requirements: `Episode.objects.for_podcast(show).published().with_play_counts().order_by("-published_at")`. The feed, the sitemap and the admin action "publish now" all reuse the same `published()` definition, so a change to the rule (say, hiding explicit episodes in some regions) is made in one place. Because the methods only build the query, the whole chain is still one SQL statement, evaluated when the template iterates it. ## Practical guidance - Return `self.filter(...)`, `self.exclude(...)` or `self.annotate(...)` from QuerySet methods; never evaluate inside them, or the chain stops being lazy. - Keep names domain-level (`published()`, `for_listener(user)`), not SQL-level. - Use `from_queryset()` when you need both: manager-only factories such as `create_draft(...)` plus chainable filters. - If the manager has `use_in_migrations = True`, migrations must be able to import its class, so subclass the generated class at module level (`class EpisodeManager(BaseEpisodeManager.from_queryset(EpisodeQuerySet)): pass`); otherwise Django's `deconstruct()` raises `ValueError` asking you to inherit from the dynamically generated manager. - Test the QuerySet methods directly: `Episode.objects.all().published()` against a fixture of draft, scheduled and published episodes.

  • In Django, why is a method named _visible() on a custom QuerySet missing from the manager built with as_manager()?
    Only public methods are copied onto the manager. Names starting with an underscore are skipped unless the method sets `queryset_only = False`. Rename it, or add that attribute, if it should be callable as `Episode.objects._visible()`.
  • In Django, can a custom QuerySet method use annotate() or another model's QuerySet?
    Yes. Any method that returns a QuerySet of the same model keeps the chain alive, for example `return self.annotate(plays=Count("listens"))` or `return self.filter(podcast__in=Podcast.objects.active())`. Avoid returning lists or evaluated results, which end the chain.

saying these in an interview costs you the question

  • A method on a custom Manager can be chained after filter()
  • as_manager() copies every QuerySet method, including private ones
  • from_queryset() returns a manager instance, not a class
  • podcast.episodes cannot use custom QuerySet methods
  • QuerySet methods should return a list so templates can loop
open as a page

In Django, what is a model Manager such as Episode.objects, and what changes when you declare a manager under another name?

level: juniorimportance: should knowfreq 52%

basics

~20 s

A Manager is the class-level interface Django attaches to a model for database queries; its methods return QuerySets. Django adds objects only when a model declares no manager, so naming one catalog removes objects entirely.

open as a page

In Django, what breaks when a manager that hides draft episodes is declared first on the model, and how do default_manager_name and base_manager_name help?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The first declared manager becomes the default manager, used by the admin, dumpdata, form choices, unique checks and reverse related managers, so drafts vanish from all of them. Declare a plain manager first or set Meta.default_manager_name.

open as a page