Almost every view reads or writes data, and QuerySets are how Django does it safely and readably.
Each model has a manager, objects, that produces QuerySets: lazy, chainable descriptions of a query.
Reading:
Post.objects.all(),.filter(status="pub"),.exclude(views=0).get(pk=1)returns exactly one object or raisesPost.DoesNotExist/MultipleObjectsReturned.order_by("-created", "title"),.first(),.last(),.count(),.exists()- slicing
[:10]becomesLIMIT 10;[10:20]addsOFFSET
Field lookups use double underscores: title__icontains="django", views__gte=100, created__year=2025, status__in=["pub", "draft"], body__isnull=True, slug__startswith="how".
Several keyword arguments in one filter() are combined with AND.
Writing:
Post.objects.create(title=..., ...)inserts and returns the object- change attributes then
post.save();save(update_fields=["title"])updates only those columns Post.objects.filter(status="draft").update(status="pub")runs one UPDATE without loading objectspost.delete()orqueryset.delete()get_or_create(...)andupdate_or_create(...)for idempotent writes
values("title") returns dicts and values_list("title", flat=True) plain values, which is cheaper when you don't need model instances.
QuerySets are lazy: building one runs no SQL. The query runs when you iterate, slice with a step, call len(), list(), bool() or print it. The ORM always passes values as query parameters, so filters are safe from SQL injection.
Example
from blog.models import Post
post = Post.objects.create(title="Hello Django", slug="hello", status="pub")
published = Post.objects.filter(status="pub").exclude(views=0) # no SQL yet
recent = published.order_by("-created")[:5] # still lazy
for p in recent: # SQL runs here
print(p.title)
Post.objects.filter(title__icontains="django", created__year=2025).count()
Post.objects.filter(status__in=["pub", "draft"]).exists() # True
try:
Post.objects.get(slug="nope")
except Post.DoesNotExist:
print("not found")
post.title = "Hello, Django"
post.save(update_fields=["title"])
Post.objects.filter(status="draft").update(status="pub") # one UPDATE
titles = list(Post.objects.values_list("title", flat=True)) # ['Hello, Django', ...]
print(Post.objects.filter(status="pub").query) # SELECT ... WHERE "blog_post"."status" = pub ...Remember
- filter/exclude/order_by chain lazily; SQL runs on iteration or evaluation
- field__lookup=value expresses conditions (icontains, gte, in, year...)
- get() raises DoesNotExist; update()/delete() work on whole QuerySets
Watch out for
- Using get() for something that may be missing without handling DoesNotExist (or get_object_or_404 in views).