Querying the ORM

Every model gets a manager, objects, which is your entry point for reading data — filtering, sorting, looking up a single row — all written as Python method calls instead of raw SQL.

Getting everything: .all()

Python Django shell
>>> from blog.models import Post
>>> Post.objects.all()
<QuerySet [<Post: Hello, Django>, <Post: Second post>, <Post: Draft idea>]>

Post.objects.all() returns a QuerySet, not a plain Python list — it behaves like one (you can loop over it, slice it, count it) but represents a database query that hasn't necessarily run yet.

Filtering: .filter() and .exclude()

Python Django shell
>>> Post.objects.filter(published=True)
<QuerySet [<Post: Hello, Django>, <Post: Second post>]>

>>> Post.objects.exclude(published=True)
<QuerySet [<Post: Draft idea>]>

>>> Post.objects.filter(title__contains="Django")
<QuerySet [<Post: Hello, Django>]>

filter() keeps only matching rows; exclude() keeps everything else. The double-underscore in title__contains is a field lookup — Django's way of expressing "contains," "greater than" (__gt), "starts with" (__startswith), and dozens of others as keyword arguments, each translated into the appropriate SQL.

Getting exactly one row: .get()

Python Django shell
>>> Post.objects.get(id=1)
<Post: Hello, Django>

>>> Post.objects.get(id=999)
Traceback (most recent call last):
    ...
blog.models.DoesNotExist: Post matching query does not exist.

Unlike filter(), which always returns a QuerySet (even an empty one), get() returns a single object directly — and raises an exception if zero or more than one row matches. Use get() only when you're confident exactly one row should match, typically by a unique field like id.

Chaining and laziness

Python Django shell
>>> recent_published = Post.objects.filter(published=True).order_by('-created_at')[:5]

QuerySet methods chain because each one returns another QuerySet — filter() then order_by() then a slice, all combined into a single SQL query. Crucially, none of that query actually runs against the database until you do something that needs the results: looping over it, calling len(), or printing it. This is called lazy evaluation.

Note: laziness is usually a performance win — Django only builds the SQL it actually needs — but it hides a common trap: looping over a related object's queryset inside another loop (e.g. post.comments.all() inside a loop over posts) runs one extra query per iteration, known as the "N+1 query problem." The fix, select_related()/prefetch_related(), is worth knowing exists even if it's beyond this introductory lesson.