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()
>>> 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()
>>> 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()
>>> 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
>>> 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.
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.