At the heart of our mission to lead Europe into a clean, electrified future is our thriving marketplace connecting homeowners with installers. This platform is built on the robustness of the Django web framework.
Central to Django are its models — the foundational blocks that most things revolve around.
As developers, we often need to augment models with additional table-level logic: accessing, filtering, or annotating data in convenient ways. A common first instinct might be to define static methods directly on the model. While this can work, Django offers a much better and more idiomatic alternative: managers and querysets.
In this post, we’ll explore how custom managers and custom querysets help produce cleaner, more maintainable code — and when each pattern makes sense.
Why static methods aren’t ideal
Let’s start with a naive example:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
is_published = models.BooleanField(default=False)
@staticmethod
def get_published_books():
return Book.objects.filter(is_published=True)
@staticmethod
def get_books_by_title(search_text):
return Book.objects.filter(title__icontains=search_text)
This works:
published_books = Book.get_published_books()
python_books = Book.get_books_by_title('Python')
But while convenient, these static methods simply rephrase what is already expressible with Django’s ORM. They don’t add much value, and worse, they blur the separation of concerns: models should describe data, not take on responsibility for querying.
If your custom filter logic is trivial (as above), it’s often better to just write:
Book.objects.filter(is_published=True)
Custom query methods shine when they hide complexity or combine multiple filters or annotations. For simple filters, they risk becoming unnecessary indirection.
Using custom querysets
A much better pattern for reusable, chainable query logic is using custom querysets.
from django.db import models
from django.db.models import Count
class BookQuerySet(models.QuerySet):
def published(self):
return self.filter(is_published=True)
def title_contains(self, text):
return self.filter(title__icontains=text)
def annotate_authors_count(self):
return self.annotate(authors_count=Count('authors'))
class Book(models.Model):
title = models.CharField(max_length=200)
is_published = models.BooleanField(default=False)
objects = BookQuerySet.as_manager()
Now, queries can be easily composed and chained:
books = Book.objects.published().title_contains('Python').annotate_authors_count()
for book in books:
print(book.title, book.authors_count)
This pattern makes your query logic expressive and powerful while keeping it close to the ORM’s natural syntax.
Custom managers (optional advanced topic)
In most cases, defining a custom queryset and using .as_manager()
is all you need.
However, Django also allows defining custom managers — subclasses of models.Manager
— which can:
- Modify the base
QuerySet
(by overridingget_queryset
) - Add additional manager-only methods (less common, mostly for non-chainable behavior)
Custom managers are useful when you want to change what .objects
returns globally (for example, filtering out soft-deleted rows) or when defining alternate “entry points” into the model’s data.
class SoftDeleteManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class Book(models.Model):
title = models.CharField(max_length=200)
is_deleted = models.BooleanField(default=False)
objects = SoftDeleteManager()
This is a more advanced use case and should be used thoughtfully. For everyday reusable query logic, prefer custom querysets.
Closing notes
Managers and querysets are powerful tools for encapsulating table-level query logic.
- Custom QuerySets (with
.as_manager()
) are the recommended approach for adding chainable, expressive query methods. - Custom Managers are best reserved for special situations (like soft deletes) where you want to globally modify a model’s query behavior or provide alternative access patterns.
- Use model methods for row-level logic acting on individual model instances.
By following these guidelines, you keep your models clean, your queries reusable and expressive, and your codebase maintainable.
About Otovo
With our vision of Otovo being the easy, trustworthy, and affordable way for homeowners to go solar, we connect homeowners with installation companies across Europe. To achieve this, we continuously evolve our platform, making full use of Django’s flexibility and power to deliver great experiences for both customers and partners.
Does this sound interesting? Check out our job openings!