In Django's ORM, what is the difference between filter() and get(), and which exceptions can get() raise?
answer
- a set versus one object
- empty is fine for one
- two model-specific exceptions
- 404 shortcut for views
basics
~10 sfilter() returns a lazy QuerySet of zero or more rows and never raises for no matches. get() runs immediately and returns exactly one instance, raising Model.DoesNotExist for none and Model.MultipleObjectsReturned for more than one.
solid answer
~30 s`Permit.objects.filter(status='active')` returns a lazy `QuerySet`; an empty result is simply an empty QuerySet. `Permit.objects.get(number='P-1042')` executes at once and must match exactly one row: none raises `Permit.DoesNotExist`, two or more raise `Permit.MultipleObjectsReturned`. Both are per-model subclasses of `django.core.exceptions.ObjectDoesNotExist` and `MultipleObjectsReturned`, so you can catch the specific or the generic one. Use `get()` for lookups by a unique field, such as the primary key or a unique permit number, and `filter()` everywhere else. In views, `get_object_or_404(Permit, number=...)` converts `DoesNotExist` into `Http404`, and `filter(...).first()` returns `None` instead of raising.
code
python · 18 linesfrom django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from permits.models import Permit
active = Permit.objects.filter(status=Permit.Status.ACTIVE) # lazy, may be empty
try:
permit = Permit.objects.get(number='P-1042') # runs now
except Permit.DoesNotExist:
permit = None
except Permit.MultipleObjectsReturned:
raise # a data or lookup bug
def permit_detail(request, number):
permit = get_object_or_404(Permit, number=number) # Http404 when missing
...go deeper
Recall that filter() gives a possibly empty QuerySet while get() gives one object or raises DoesNotExist or MultipleObjectsReturned.
Explain the per-model exception classes, the 21-row limit in get(), and when first() or get_object_or_404 fits better.
Treat MultipleObjectsReturned as a data-integrity signal: back get() lookups with unique constraints and avoid check-then-get races.
Set conventions for how services report missing objects, exceptions or None, so views and APIs map them consistently to 404s.
## Two different contracts Django's manager and `QuerySet` expose two ways to read rows that look alike but promise different things: - **`filter(**lookups)`** returns a new, **lazy** `QuerySet` describing all rows that match. It runs no SQL until evaluated, and a query that matches nothing is just an empty QuerySet: iterating it does nothing, `exists()` is `False`, no exception. - **`get(**lookups)`** **executes immediately** and returns **one model instance**. Its contract is "exactly one row matches", and it raises when that is not true. Both accept the same keyword lookups (`number='P-1042'`, `zone__code='A'`, `valid_from__gte=date(2026, 1, 1)`) and the same positional `Q` objects. ## The exceptions get() raises | Situation | Exception | Message shape | |---|---|---| | no row matches | `Permit.DoesNotExist` | `Permit matching query does not exist.` | | two or more rows match | `Permit.MultipleObjectsReturned` | `get() returned more than one Permit -- it returned 2!` | Each model class gets its **own** exception classes, created when the model class is built: - `Permit.DoesNotExist` subclasses `django.core.exceptions.ObjectDoesNotExist`. - `Permit.MultipleObjectsReturned` subclasses `django.core.exceptions.MultipleObjectsReturned`. Catching `Permit.DoesNotExist` handles only permits; catching `ObjectDoesNotExist` handles any model, which is useful in generic code that looks up several models. ## How get() runs 1. It applies the lookups as `filter()` would. 2. It clears any ordering, since order does not matter for a single row. 3. It limits the query to **21 rows**, so an accidental match on a huge table does not load everything; the error then says it returned "more than 20". 4. It evaluates, then returns the single row or raises. Because of step 3, `MultipleObjectsReturned` is cheap to detect, but it is still a **bug signal**: either the lookup was not unique or the data is not what you believed. ## Choosing between them - Use `get()` when the lookup is **unique by design**: primary key, a `unique=True` field such as the permit number, or a `UniqueConstraint` combination. A duplicate would be a data error worth raising on. - Use `filter()` when zero, one or many rows are all legitimate: search results, a holder's permits, active permits in a zone. - Use `filter(...).first()` when "maybe one" is normal and `None` is a convenient answer; it orders by primary key if the QuerySet has no ordering, so the choice is deterministic. - Use `filter(...).exists()` when you only need to know whether a match exists. ## In views - `get_object_or_404(Permit, number=number)` calls `get()` and turns `DoesNotExist` into `Http404`. It does **not** catch `MultipleObjectsReturned`; a non-unique lookup still becomes a server error, which is appropriate because it is a programming error. - `get_list_or_404(Permit, holder=request.user)` is the `filter()` counterpart, raising `Http404` when the list is empty. - Catching `DoesNotExist` around `get()` is fine in services; a bare `except Exception` hides real errors and is not. ## get() compared with its neighbours | Call | Runs SQL | No match | Several matches | |---|---|---|---| | `filter(...)` | when evaluated | empty QuerySet | all of them | | `get(...)` | immediately | `DoesNotExist` | `MultipleObjectsReturned` | | `filter(...).first()` | immediately | `None` | the first by ordering, or by `pk` | | `filter(...)[0]` | immediately | `IndexError` | whichever the database returns first | | `get_object_or_404(...)` | immediately | `Http404` | `MultipleObjectsReturned` | The table is also a checklist for code review: every read of "one permit" should say which of these failure behaviours it wants. A permit lookup by number in a public view wants a 404; a background job that must find exactly one permit wants the exception, so the job fails loudly instead of acting on the wrong row. ## Unique lookups and constraints `get()` is only as safe as the uniqueness behind it. If the permit number is declared `unique=True`, the database guarantees at most one row and `MultipleObjectsReturned` cannot happen for that lookup. If uniqueness is merely a convention in application code, concurrent inserts can create duplicates, and `get()` becomes the place where that data bug finally surfaces. ## Common mistakes - **`get()` on a non-unique field**, such as a plate number that can appear on several permits; it works in development and raises in production the day a duplicate appears. - **`filter(...)[0]`** used as "get one": it raises `IndexError` on no match and silently picks one of several on duplicates. - **Testing `if Permit.objects.filter(...)` then calling `get()`**: two queries, and a race window between them. - **Forgetting that `get()` is eager**: it cannot be chained further, because it returns an instance, not a QuerySet.
- Why does get() limit its query instead of fetching every match?It only needs to know whether there are zero, one or several matches. Django sets a limit of 21 rows, so a lookup that accidentally matches a million rows costs a small query; the `MultipleObjectsReturned` message then says 'more than 20' rather than the exact count. On backends where `select_for_update()` cannot be combined with a limit, the limit is skipped.
- Should a service function catch ObjectDoesNotExist or Permit.DoesNotExist?Catch the model-specific `Permit.DoesNotExist` when the code looks up one known model: it documents intent and cannot swallow a missing Zone from a nested lookup. Catch `ObjectDoesNotExist` only in generic code that handles several models the same way, such as a helper that resolves objects by content type.
saying these in an interview costs you the question
- filter() raises DoesNotExist when nothing matches
- get() returns None when no row matches
- get_object_or_404 also turns MultipleObjectsReturned into a 404
- get() loads every matching row before complaining about duplicates
- DoesNotExist is one shared class for all models