Django Model Managers, QuerySets & Properties

Blog / Django · November 8, 2020 · Updated June 10, 2026 · 8 min read
Django Model Managers, QuerySets & Properties

In Django, almost every database interaction flows through two related layers: Managers and QuerySets. A Manager is the entry point you reach through Model.objects, while a QuerySet is the lazy, chainable collection of rows it builds. Add model properties on top — Python attributes computed from a single instance — and you have the three building blocks that keep query logic and derived data out of your views and templates.

This guide is a conceptual tour for Django 5.x: what a Manager actually is, how custom Managers and custom QuerySets differ, why Manager.from_queryset() is usually the right tool, and where model @property/@cached_property fit (and where they don't). If you just want the step-by-step recipe for wiring up one custom manager, see our focused walkthrough on adding a custom manager in Django.

What a Manager is

A Manager is the interface through which database query operations are provided to a model. Every model gets one automatically, named objects. When you write Article.objects.all() or Article.objects.filter(...), you are calling methods on that Manager, and each call hands back a fresh QuerySet.

You can declare the default Manager explicitly — it is identical to the implicit one Django adds for you:

from django.db import models


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)

    objects = models.Manager()  # Same as the manager Django adds implicitly.

    def __str__(self):
        return self.title

Default vs. additional managers

A model can have more than one Manager. The first one defined in the class body becomes the model's default manager — exposed as Model._default_manager and used by parts of Django such as related lookups, forms, and serialization. There is also a base manager (Model._base_manager), used internally for things like following reverse relations, which deliberately ignores custom get_queryset() filtering so related objects are never silently hidden.

You can control both names in Meta instead of relying on declaration order:

  • Meta.default_manager_name — pick which manager is the default.
  • Meta.base_manager_name — pick which manager backs related-object access.

Gotcha: because the first manager declared becomes _default_manager, never put a filtering manager first if you also keep an unfiltered one. Keep objects = models.Manager() at the top so the full, unfiltered set stays the default — otherwise admin, migrations, and related lookups may quietly operate on a filtered subset.

Custom Managers: overriding get_queryset()

The most common customization is narrowing the initial QuerySet a manager returns by overriding get_queryset(). A classic example is a PublishedManager that only ever sees published rows. Note how objects stays first so it remains the default manager, with published added as an extra:

from django.db import models
from django.utils import timezone


class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(
            is_published=True,
            published_at__lte=timezone.now(),
        )


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)

    objects = models.Manager()       # First -> default manager (unfiltered).
    published = PublishedManager()   # Extra manager: only live articles.

    def __str__(self):
        return self.title

Now Article.published.all() returns only live articles, while Article.objects.all() still returns everything. You can also add table-level methods directly to a Manager — for example a title_count(keyword) helper — which is the right home for logic that operates across rows rather than on one instance. For a full step-by-step on building one of these, see how to add a custom manager in Django.

Custom QuerySets and Manager.from_queryset()

Manager-only methods have a well-known limitation: they are not chainable. If Article.published is the entry point, you can't write Article.published.this_month().by_author(user) unless every method also lives on the QuerySet — because chaining happens on the QuerySet returned by the previous call, not on the Manager.

The fix is to put your reusable query logic on a custom QuerySet and then expose it through a Manager with Manager.from_queryset(). The methods become available on both the manager and any QuerySet it produces, so they chain freely:

from django.db import models
from django.utils import timezone


class ArticleQuerySet(models.QuerySet):
    def published(self):
        return self.filter(is_published=True, published_at__lte=timezone.now())

    def by_author(self, user):
        return self.filter(author=user)

    def this_month(self):
        now = timezone.now()
        return self.filter(published_at__year=now.year, published_at__month=now.month)


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)

    # Build a Manager that carries every QuerySet method, and call it () to instantiate.
    objects = ArticleQuerySet.as_manager()

    def __str__(self):
        return self.title

With that in place, everything chains:

Article.objects.published().this_month().by_author(request.user)

QuerySet.as_manager() is shorthand when you only need the QuerySet methods. If you also want manager-only methods or a custom get_queryset(), use Manager.from_queryset() to build the base class and subclass it:

class ArticleManager(models.Manager.from_queryset(ArticleQuerySet)):
    def get_queryset(self):
        # Restrict the default set, while keeping all chainable QuerySet methods.
        return super().get_queryset().filter(is_archived=False)

    def bulk_archive(self, ids):
        # A manager-only "table-level" helper.
        return self.filter(id__in=ids).update(is_archived=True)


class Article(models.Model):
    # ... fields ...
    objects = ArticleManager()

Why prefer QuerySet methods over manager-only methods? They are reusable across chains, they work after .filter()/.exclude(), and they keep a single source of truth for query logic. Reach for from_queryset() whenever a method needs to be callable in the middle of a chain. For the filtering vocabulary these methods build on, see querying with Django Q objects.

Model properties: computed, read-only attributes

The "properties" half of the title is about per-instance computed values. A standard Python @property turns a method into a read-only attribute — perfect for values derived from fields already loaded on the instance:

from functools import cached_property
from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    @property
    def full_name(self):
        # Cheap, derived from already-loaded fields. Recomputed on each access.
        return f"{self.first_name} {self.last_name}"

    @cached_property
    def lifetime_order_total(self):
        # Expensive: hits the DB. Computed once per instance, then memoized.
        return self.orders.aggregate(total=models.Sum("amount"))["total"] or 0

Use @property for cheap derivations and @cached_property (from functools) for expensive work — a DB aggregate, an API call, a heavy calculation — that you want to run once per instance and reuse. The cache lives on the instance, so a freshly fetched object recomputes it.

Key caveat: properties are pure Python and run after rows are loaded. They are not queryable — you cannot filter(), order_by(), or annotate() on full_name, because the database has never heard of it. Anything you need to filter or sort on must be computed in the database.

When you need it in the database: annotations, F(), and GeneratedField

If a derived value must be filterable or sortable, compute it DB-side instead of in a property:

  • annotate() + expressions — add a computed column to a QuerySet on the fly (great inside a QuerySet method).
  • F() expressions — reference and combine columns in queries and updates without pulling values into Python.
  • GeneratedField (added in Django 5.0) — a real database-generated column whose value the DB computes from other fields, so it is always consistent and fully queryable.
from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat


class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    # Django 5.0+: a stored, queryable column computed by the database.
    full_name = models.GeneratedField(
        expression=Concat("first_name", Value(" "), "last_name"),
        output_field=models.CharField(max_length=101),
        db_persist=True,
    )


# Or compute on the fly with annotate() when you don't want a stored column:
Person.objects.annotate(
    name=Concat("first_name", Value(" "), "last_name")
).filter(name__icontains="dahl").order_by("name")

Best practices

  • Fat models, thin views. Keep business query logic in QuerySet/Manager methods so views read like sentences (Article.objects.published().for_team(team)) and the logic is reusable and testable.
  • Avoid N+1 queries. Bake select_related (for forward FK/one-to-one) and prefetch_related (for many-to-many/reverse FK) into manager/QuerySet methods so callers can't forget them.
  • Properties for display, the ORM for filtering. Use @property/@cached_property for read-only derived display values; push anything filterable into annotations or a GeneratedField.
  • Keep objects first when you add filtered managers, so the default/base managers stay unfiltered.
  • Need behavioural variants of a model without a new table? A proxy model is often cleaner than a second manager — see overriding model behaviour with proxy models. And for tuning the queries your managers emit, our guide to Django database access optimization goes deeper.
class ArticleQuerySet(models.QuerySet):
    def with_related(self):
        # Ship the joins/prefetches from the manager so callers avoid N+1 by default.
        return self.select_related("author").prefetch_related("tags", "comments")

    def published(self):
        return self.filter(is_published=True)


class Article(models.Model):
    # ... fields ...
    objects = ArticleQuerySet.as_manager()


# One query for articles + authors, plus batched queries for tags/comments:
for article in Article.objects.published().with_related():
    print(article.author.username, article.title)

When you need help designing a maintainable Django data layer — managers, query optimization, and a model architecture that scales — our team offers hands-on Django development services.

Frequently Asked Questions

What is the difference between a Manager and a QuerySet in Django?

A Manager is the interface exposed on the model (via Model.objects) that creates QuerySets; a QuerySet is the lazy, chainable collection of rows it returns. Methods on a Manager are called from the model class, while QuerySet methods chain off each other (.filter().exclude().order_by()). Put reusable query logic on a QuerySet so it stays chainable.

Why should I use Manager.from_queryset() instead of methods on the Manager?

Manager-only methods break chaining — after the first call you have a QuerySet, which doesn't have your custom methods. Manager.from_queryset(MyQuerySet) (or MyQuerySet.as_manager()) copies the QuerySet methods onto the manager, so the same methods work as the entry point and mid-chain, with one source of truth.

Why does the order in which I declare managers matter?

The first Manager defined on a model becomes its _default_manager, which Django uses for related lookups, forms, admin, and migrations. If you declare a filtering manager first, those systems silently operate on the filtered subset. Keep objects = models.Manager() first and add filtered managers (like published) after it.

Can I filter or order a queryset by a model @property?

No. A @property is plain Python evaluated after rows load, so the database can't see it — filter(), order_by(), and annotate() won't work on it. For database-side computed values, use annotate() with an expression, an F() expression, or a GeneratedField (Django 5.0+), all of which are queryable.

When should I use @cached_property instead of @property?

Use @property for cheap values derived from already-loaded fields. Use functools.cached_property when the computation is expensive (a DB aggregate, an API call, heavy math) and you want it to run once per instance and be reused. The cached value lives on that instance, so a newly fetched object recomputes it.

How do custom managers help avoid N+1 query problems?

Bake select_related (forward FK/one-to-one) and prefetch_related (many-to-many/reverse FK) into a QuerySet or manager method — for example with_related(). Callers then get the joins and prefetches automatically instead of triggering a query per related object in a loop. See our Django database access optimization guide for more.

Share this article