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
- What a Manager is and that Model.objects is the query gateway
- How a manager returns a QuerySet via get_queryset()
- Adding helper methods like published() to a custom Manager
- Overriding get_queryset() to change the default set
- Building a chainable custom QuerySet subclass
- Using as_manager() so methods chain after filter()
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 rowsThe 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 Notes2 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 onlyYou 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
| Construct | What It Does |
|---|---|
| Model.objects | The 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
- Previous: Query Performance: select_related & prefetch_related
- Next: Custom Template Tags & Filters — Extend templates with your own filters, simple tags, and inclusion tags
- Quick reference: Django cheat sheet
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.