Custom Managers & QuerySets

Reviewed & published by Brayan K

A Django Manager is the interface attached to a model as Model.objects that provides database query operations and returns a QuerySet, making it the gateway through which every query begins.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll see what the default objects manager really is, write a custom Manager that adds methods like published() and overrides get_queryset(), and build a chainable custom QuerySet with as_manager() so calls like Post.objects.published().recent() just work.

What You'll Learn in This Lesson

1 The Default objects Manager

Every Django model you define automatically gets a Manager attached under the name objects. A manager is the object that provides query operations — it is the gateway to the database. When you write Post.objects.all() or Post.objects.filter(...), you are calling methods on that manager, and what comes back is a QuerySet representing rows.

# blog/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")

# Django adds this for you automatically:
#     objects = models.Manager()
# so every query starts at Post.objects

# In a shell or a view:
Post.objects.all()                  # a QuerySet of every row
Post.objects.filter(status="published")
Post.objects.count()

# Under the hood every one of those calls get_queryset() first:
#   Post.objects.get_queryset()  ->  the base QuerySet of all rows

The manager itself holds very little state. Its key method is get_queryset(), which builds and returns the base QuerySet that all(), filter(), and friends start from.

# Model.objects is the default Manager: the gateway to building querysets.
# Here is a tiny Manager wrapping a list of rows, mirroring how
# Post.objects gives you access to all the Post rows.

class Manager:
    def __init__(self, rows):
        self.rows = rows

    def get_queryset(self):
        return list(self.rows)

    def all(self):
        return self.get_queryset()

    def count(self):
        return len(self.get_queryset())

# Each row is one "Post" in the database
posts = [
    {"title": "Hello World", "status": "published"},
    {"title": "Draft Idea", "status": "draft"},
    {"title": "Release Notes", "status": "published"},
]

# Post.objects is an instance of this Manager
objects = Manager(posts)

print("Total posts:", objects.count())
for post in objects.all():
    print("-", post["title"])

# EXPECTED:
# Total posts: 3
# - Hello World
# - Draft Idea
# - Release Notes

# ✅ Expected output:
# Total posts: 3
# - Hello World
# - Draft Idea
# - Release Notes

2 A Custom Manager: published() & Overriding get_queryset()

To stop repeating filter(status="published") everywhere, you subclass models.Manager and add a method that wraps it. Inside the method you call self.get_queryset() and filter that, so Post.objects.published() reads like plain English.

# blog/models.py
from django.db import models

class PostManager(models.Manager):
    def published(self):
        # wrap the default queryset and filter it
        return self.get_queryset().filter(status="published")

class Post(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")

    objects = PostManager()   # replace the default manager

# Now this works everywhere:
Post.objects.published()      # -> QuerySet of published posts only

You can also override get_queryset() itself to change the default set the manager returns. A LiveManager whose get_queryset() excludes drafts means every query through it is already filtered:

class LiveManager(models.Manager):
    def get_queryset(self):
        # change the DEFAULT set for this manager
        return super().get_queryset().exclude(status="draft")

class Post(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")

    objects = models.Manager()   # the unfiltered, default manager
    live = LiveManager()         # a second manager that hides drafts

Post.objects.all()   # every post, including drafts
Post.live.all()      # drafts already excluded
# A custom Manager adds query methods like published() and can override
# get_queryset() to change the default set. published() wraps
# get_queryset().filter(status="published") so Post.objects.published() works.

class PostManager:
    def __init__(self, rows):
        self.rows = rows

    def get_queryset(self):
        # the default set the manager starts from
        return list(self.rows)

    def filter_status(self, queryset, status):
        return [row for row in queryset if row["status"] == status]

    def published(self):
        return self.filter_status(self.get_queryset(), "published")


class LiveManager(PostManager):
    def get_queryset(self):
        # override the default set to exclude drafts entirely
        base = super().get_queryset()
        return [row for row in base if row["status"] != "draft"]


posts = [
    {"title": "Hello World", "status": "published"},
    {"title": "Draft Idea", "status": "draft"},
    {"title": "Release Notes", "status": "published"},
    {"title": "Archived Post", "status": "archived"},
]

objects = PostManager(posts)
live = LiveManager(posts)

print("published():", [p["title"] for p in objects.published()])
print("live default:", [p["title"] for p in live.get_queryset()])

# EXPECTED:
# published(): ['Hello World', 'Release Notes']
# live default: ['Hello World', 'Release Notes', 'Archived Post']

# ✅ Expected output:
# published(): ['Hello World', 'Release Notes']
# live default: ['Hello World', 'Release Notes', 'Archived Post']

3 Chainable Custom QuerySets with as_manager()

A method on a manager can only be called first — Post.objects.published() works, but Post.objects.filter(...).published() does not, because filter() returns a plain QuerySet that has never heard of published(). The fix is to put the methods on a custom QuerySet subclass, where each method returns another QuerySet and therefore chains.

# blog/models.py
from django.db import models

class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status="published")   # returns a QuerySet

    def recent(self):
        return self.order_by("-created_at")       # also a QuerySet

class Post(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20, default="draft")
    created_at = models.DateTimeField(auto_now_add=True)

    # as_manager() turns the QuerySet into a Manager
    objects = PostQuerySet.as_manager()

# Because every method returns a QuerySet, they all chain:
Post.objects.published().recent()
Post.objects.filter(title__icontains="django").published()

PostQuerySet.as_manager() builds a manager whose methods come from the QuerySet, so the same logic is available straight off objects and after any filter() — no duplication between a manager and a queryset.

# A custom QuerySet has chainable methods that each return a new (or the
# same) queryset, so you can write Post.objects.published().recent().
# as_manager() turns the QuerySet into a Manager so the methods are reusable.

class PostQuerySet:
    def __init__(self, rows):
        self.rows = list(rows)

    def published(self):
        rows = [r for r in self.rows if r["status"] == "published"]
        return PostQuerySet(rows)        # return a queryset -> chainable

    def recent(self):
        rows = sorted(self.rows, key=lambda r: r["year"], reverse=True)
        return PostQuerySet(rows)        # also returns a queryset

    def titles(self):
        return [r["title"] for r in self.rows]


posts = [
    {"title": "Old News", "status": "published", "year": 2019},
    {"title": "Draft Idea", "status": "draft", "year": 2024},
    {"title": "Fresh Post", "status": "published", "year": 2026},
    {"title": "Mid Post", "status": "published", "year": 2022},
]

# objects acts like Post.objects.all()
objects = PostQuerySet(posts)

# Chain the methods, just like Post.objects.published().recent()
result = objects.published().recent()
print("Chained:", result.titles())

# EXPECTED:
# Chained: ['Fresh Post', 'Mid Post', 'Old News']

# ✅ Expected output:
# Chained: ['Fresh Post', 'Mid Post', 'Old News']

🎯 Your Turn: Finish the published() Filter

Fill in the blank so published() keeps only the rows whose status equals "published".

# 🎯 YOUR TURN: replace ___ so published() keeps only rows whose
# status equals "published".

class PostQuerySet:
    def __init__(self, rows):
        self.rows = list(rows)

    def published(self):
        rows = [r for r in self.rows if r["status"] ___ "published"]
        return PostQuerySet(rows)

    def titles(self):
        return [r["title"] for r in self.rows]

posts = [
    {"title": "Hello World", "status": "published"},
    {"title": "Draft Idea", "status": "draft"},
    {"title": "Release Notes", "status": "published"},
]

print(PostQuerySet(posts).published().titles())

# EXPECTED OUTPUT:
# ['Hello World', 'Release Notes']

Common Errors (And How to Fix Them)

❌ "QuerySet object has no attribute 'published'"

You put published() on a Manager, then called it after filter() — but filter() returns a plain QuerySet.

✅ Fix: define the method on a QuerySet subclass and use objects = PostQuerySet.as_manager() so it chains.

❌ Rows vanish from the admin after a custom manager

You overrode get_queryset() on the default objects manager, so the admin and related lookups inherited the filter.

✅ Fix: keep objects = models.Manager() unfiltered and add the filtered behaviour under a second name like live.

❌ Manager method returns a list, breaking later .filter()

Your method returned list(...) instead of a QuerySet, so chaining a queryset method afterwards fails.

✅ Fix: always return self.get_queryset().filter(...) (a QuerySet), never a materialised list.

📋 Quick Reference

ConstructWhat It Does
Model.objectsThe default manager — the gateway to every query
get_queryset()Builds the base QuerySet the manager starts from
class PostManager(models.Manager)Custom manager for extra methods like published()
class PostQuerySet(models.QuerySet)Custom QuerySet whose methods are chainable
PostQuerySet.as_manager()Turns a QuerySet into a manager so methods chain after filter()

🏆 Mini Challenge: Chain published().recent()

Build a chainable QuerySet so that objects.published().recent() returns only published posts, newest first. The methods must each return a QuerySet so they can be chained.

# 🏆 Mini Challenge: build a chainable QuerySet so that
# objects.published().recent() returns published posts, newest first.

class PostQuerySet:
    def __init__(self, rows):
        self.rows = list(rows)

    def published(self):
        rows = [r for r in self.rows if r["status"] == "published"]
        return PostQuerySet(rows)

    def recent(self):
        rows = sorted(self.rows, key=lambda r: r["year"], reverse=True)
        return PostQuerySet(rows)

    def as_manager(self):
        # as_manager() exposes the same methods on a manager
        return self

posts = [
    {"title": "Vintage", "status": "published", "year": 2018},
    {"title": "Secret Draft", "status": "draft", "year": 2027},
    {"title": "Latest", "status": "published", "year": 2026},
    {"title": "Middle", "status": "published", "year": 2021},
]

objects = PostQuerySet(posts).as_manager()
chained = objects.published().recent()
print("Result:", [r["title"] for r in chained.rows])

# EXPECTED:
# Result: ['Latest', 'Middle', 'Vintage']

❓ Frequently Asked Questions

Lesson complete — you control how your models are queried!

You now know that Model.objects is a manager and the gateway to every query, how to add methods like published() and override get_queryset() on a custom Manager, and how a chainable QuerySet subclass exposed with as_manager() lets you write Post.objects.published().recent().

Practice quiz

What name does Django give a model's default manager?

  • objects
  • manager
  • rows
  • query

Answer: objects. Every model gets a default manager called objects.

What does a manager return when you call all() or filter()?

  • A list
  • A QuerySet
  • A dictionary
  • A Model instance

Answer: A QuerySet. Managers hand back a QuerySet representing rows.

Which manager method builds the base QuerySet it starts from?

  • build_query()
  • base()
  • get_queryset()
  • all()

Answer: get_queryset(). get_queryset() returns the base QuerySet for the manager.

Where should chainable query logic like published() live to chain after filter()?

  • In the view
  • On a custom QuerySet subclass
  • On a plain Manager
  • In settings.py

Answer: On a custom QuerySet subclass. Methods on a QuerySet subclass return QuerySets, so they chain.

What does QuerySet.as_manager() do?

  • Deletes the manager
  • Runs the query immediately
  • Builds a Manager whose methods come from the QuerySet
  • Returns a list of rows

Answer: Builds a Manager whose methods come from the QuerySet. as_manager() turns a custom QuerySet into a Manager.

Why is overriding get_queryset() on the default objects manager risky?

  • It speeds up queries too much
  • It can hide rows from the admin and related lookups
  • It deletes the database
  • It is not allowed in Django

Answer: It can hide rows from the admin and related lookups. A filtered objects manager hides rows everywhere, including the admin.

What subclass do you extend to add a custom manager?

  • models.Model
  • models.QuerySet
  • models.Manager
  • models.Field

Answer: models.Manager. A custom manager subclasses models.Manager.

Why does Post.objects.filter(...).published() fail when published() is only on the Manager?

  • filter() returns a plain QuerySet that lacks published()
  • objects is read-only
  • published() needs arguments
  • filter() deletes the manager

Answer: filter() returns a plain QuerySet that lacks published(). filter() returns a plain QuerySet, which never had published().

A custom manager method should return what to allow further chaining?

  • A materialised list
  • None
  • A string
  • A QuerySet

Answer: A QuerySet. Returning a QuerySet (not a list) keeps later .filter() working.

How do projects commonly keep objects unfiltered but still hide drafts?

  • Delete draft rows
  • Disable the admin
  • Add a second manager like live with the filter
  • Override objects.get_queryset()

Answer: Add a second manager like live with the filter. Keep objects unfiltered and add a second filtered manager such as live.

Continue this course

Frequently asked questions

What is a Django Manager?

A Manager is the interface through which database query operations are provided to a Django model. Every model gets a default manager called objects, and Model.objects is the starting point for every query you write — for example Post.objects.all() or Post.objects.filter(...). The manager's job is to return a QuerySet, which is the lazy, chainable object that actually represents rows from the database.

What is the difference between a Manager and a QuerySet?

A Manager is attached to the model class (as Model.objects) and is the entry point for queries; a QuerySet is the object a manager hands back, representing a set of rows that you can filter, order, and chain. Methods you add to a Manager are only available on the manager itself, while methods you add to a custom QuerySet are chainable and stay available after every filter, which is why chainable logic belongs on a QuerySet.

When should I override get_queryset() on a manager?

Override get_queryset() when you want to change the default set of rows the manager returns for every query. A classic example is a LiveManager whose get_queryset() excludes drafts, so Post.objects always hides unpublished posts. Be careful: if you replace the default objects manager this way you can accidentally hide rows from the admin and from related lookups, so many projects add the filtered manager under a second name and keep objects unfiltered.

How does QuerySet.as_manager() work?

as_manager() is a class method on a custom QuerySet that builds a Manager whose methods come from that QuerySet. You define your chainable methods once on the QuerySet, then write objects = PostQuerySet.as_manager() on the model. The result is that the methods are available both straight off the manager (Post.objects.published()) and after any filter (Post.objects.filter(...).published()), so you never duplicate query logic between a manager and a queryset.