skip to content

In Django, when do you choose OneToOneField over ForeignKey, and what does its reverse accessor do when no related row exists?

level: middleimportance: should knowfreq 48%

answer

  1. a unique foreign key
  2. one object back, not a manager
  3. lowercased model name as accessor
  4. an exception that is also AttributeError

basics

~20 s

OneToOneField is a unique foreign key whose reverse side returns a single object instead of a manager. Use it when each row has at most one partner. When no partner exists, the reverse accessor raises RelatedObjectDoesNotExist.

solid answer

~40 s

`OneToOneField` is conceptually a `ForeignKey` with `unique=True`, but its reverse side returns one object rather than a related manager, which is why Django's checks warn (`fields.W342`) when you write `ForeignKey(unique=True)`. I use it to split optional or rarely read data off a model, such as a book's print specification, and it also underlies multi-table inheritance. It still needs `on_delete`. The reverse accessor defaults to the lowercased model name, so `book.printspec`; if no row exists it raises `RelatedObjectDoesNotExist`, a subclass of both the related model's `DoesNotExist` and `AttributeError`. That is why `hasattr(book, "printspec")` returns `False` instead of raising, and why views usually catch the exception.

code

python · 19 lines
python
from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=300)


class PrintSpec(models.Model):
    book = models.OneToOneField(Book, on_delete=models.CASCADE, primary_key=True)
    trim_size = models.CharField(max_length=20)
    page_count = models.PositiveIntegerField()


def page_count_or_none(book):
    try:
        return book.printspec.page_count
    except PrintSpec.DoesNotExist:
        # Book.printspec.RelatedObjectDoesNotExist subclasses PrintSpec.DoesNotExist
        return None

go deeper

for a junior

Recall that OneToOneField links each row to at most one partner and still requires on_delete.

for a middle

Explain how its reverse side differs from ForeignKey, the default accessor name, and the exception raised when no row exists.

for a senior

Decide when splitting a model into a one-to-one pays off, which side holds the key and cascades, and how views handle the missing row.

for a principal

Weigh a one-to-one split against a wider table or a future one-to-many, knowing that changing the cardinality later rewrites every caller.

## What OneToOneField is `OneToOneField(to, on_delete, ...)` declares a relation where each row on one side matches **at most one** row on the other. In the database it is a foreign key column with a **unique** constraint. Django's docs describe it as conceptually a `ForeignKey` with `unique=True`, with one important difference: the **reverse side returns a single object**, not a related manager. | | `ForeignKey` | `ForeignKey(unique=True)` | `OneToOneField` | |---|---|---|---| | Rows per target | many | one | one | | Reverse side | manager: `book.printspec_set.all()` | manager, even though at most one row can exist | object: `book.printspec` | | System check | none | warning `fields.W342`, suggesting `OneToOneField` | none | | `on_delete` | required | required | required | ## When to reach for it - **Splitting a model.** In a publisher's catalog, a `Book` is read constantly, while its print specification (trim size, paper stock, page count) is read only by production staff. A `PrintSpec` with `book = OneToOneField(Book, on_delete=models.CASCADE)` keeps the hot table narrow and the optional data separate. - **Extending a model you do not own**, such as a model from a third-party app, without editing it. - **Sharing the primary key.** `OneToOneField(Book, on_delete=models.CASCADE, primary_key=True)` makes the book's id the spec's id, so the two rows share one key. - **Multi-table inheritance** is built on it: Django adds an implicit `OneToOneField` from child to parent. The inheritance styles themselves are a separate subject. Use a plain `ForeignKey` instead when a second row per target is plausible in the future: turning a one-to-one into a one-to-many later changes every caller from `book.printspec` to a manager. ## The reverse accessor If you do not set `related_name`, the reverse accessor is the **lowercased model name**: `book.printspec` (not `printspec_set`). The first read queries the database and caches the result on the instance, hit or miss; it returns the `PrintSpec`, or raises when there is none. The exception is `RelatedObjectDoesNotExist`, created per relation and reachable as `Book.printspec.RelatedObjectDoesNotExist`. It subclasses both: 1. `PrintSpec.DoesNotExist`, so `except PrintSpec.DoesNotExist:` catches it; 2. `AttributeError`, so `hasattr(book, "printspec")` returns `False` instead of raising, and `getattr(book, "printspec", None)` gives `None`. The forward side behaves differently: `spec.book` on a row whose non-nullable key is set simply returns the book. On a nullable one-to-one, an unset key returns `None`. ## Delete rules still apply A one-to-one is a foreign key, so everything about `on_delete` carries over. `PrintSpec.book = CASCADE` removes the spec with its book, which suits a dependent detail row. If the second row is the more important one, flip the direction: the model that must not lose data should not be the one holding a cascading key. ## Handling the missing row Pick one pattern per code path and use it consistently: 1. **`try` / `except PrintSpec.DoesNotExist`** when the absence needs its own branch, such as showing a "spec not entered yet" notice. 2. **`getattr(book, "printspec", None)`** when `None` is a fine stand-in; it works because the exception is also an `AttributeError`. 3. **Templates** need no special handling to avoid a crash: `DoesNotExist` exceptions are marked to fail silently during variable lookup, so `{{ book.printspec.page_count }}` renders the engine's empty value. That silence can hide missing data, so test the missing case explicitly. After the first access the result, row or miss, is cached on that `book` instance, so repeated checks in one request do not repeat the query. ## Common mistakes - Writing `ForeignKey(unique=True)` and then calling `book.printspec_set.first()` everywhere; the check framework already suggests `OneToOneField`. - Accessing `book.printspec` in a view without handling the missing case, which turns every book without a spec into a server error. - Putting the cascading key on the wrong model: the row that holds the `OneToOneField` is the one deleted with its partner, so it should be the dependent detail, not the record you must keep. Interviewers use this to check that a candidate knows the reverse side is an object, not a manager, and has handled the missing-row case in real code.

  • Why does hasattr(book, "printspec") return False instead of raising when a Django book has no PrintSpec?
    The reverse accessor raises `RelatedObjectDoesNotExist`, which subclasses both `PrintSpec.DoesNotExist` and `AttributeError`. `hasattr` treats an `AttributeError` as "attribute absent" and returns `False`. The miss is cached on the instance, so a later access raises again without another query.

saying these in an interview costs you the question

  • OneToOneField needs no on_delete because only one row is involved.
  • The reverse side of a OneToOneField is a manager like book.printspec_set.
  • A missing reverse one-to-one returns None instead of raising.
  • ForeignKey(unique=True) and OneToOneField behave identically in Python.
  • OneToOneField stores the relation in a separate join table.