Models & Fields
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14
and PostgreSQL 15 or newer, all checked on 2026-10-03; where the 5.2 LTS (5.2.17) behaves differently the text says so.
Every example was run on that stack (Django 6.1.1, Python 3.13) in a scratch project on SQLite, because no PostgreSQL
server was available: the app has 54 passing tests, manage.py check reports no issues and makemigrations --check
reports no drift. Where a feature needs PostgreSQL (virtual generated columns, column and table comments, jsonb
indexes) the text says so and the SQL shown is what SQLite produced.
A model is the single, definitive description of one table of your data: its columns (fields), the rules those columns obey
(options, validators, constraints) and the behaviour that belongs to one row (methods). Almost every other page of this
section builds on it: forms are generated from models, the admin is generated from models, the ORM queries models, and
migrations are generated by diffing models. In the Bookshelf application that grows across this section the models are
Author and Book; this page defines them completely and then explains every choice made in them.
This page covers how a model is defined and loaded, every built-in field type and option, null versus blank,
enumerated choices, JSONField, database defaults and generated columns, primary keys, the Meta options, the model
methods you override most often, validation, the sync and async APIs, and how to write your own field.
The relationship fields (ForeignKey, ManyToManyField, OneToOneField) are covered in
Relationships, inheritance in
Model Inheritance & the Meta API, queries in
Making Queries, managers in
Managers & Custom QuerySets, indexes and constraints in
Constraints, Indexes & Composite Keys and schema changes in
Migrations.
In a hurry: what goes where
| Concern | Rule of thumb | Section |
|---|---|---|
One class, one table |
Subclass |
|
Choosing a field |
The most specific type that fits ( |
|
Optional values |
|
|
A closed set of values |
|
|
Semi-structured data |
|
|
Values computed by the database |
|
|
Primary keys |
Let Django add the |
|
Table-level behaviour |
|
|
Row-level behaviour |
Methods: |
|
Rules that must never be broken |
Database constraints; |
Validation: what runs and when, Pitfalls and how to avoid them |
Async code |
|
|
A column Django does not know |
Subclass |
Where models sit in the request/response cycle
A model class is defined once, when its module is imported, and from then on it is a long-lived, process-wide description of a table. The three moments that matter are start-up, request handling and the management commands that run outside any request.
AppConfig objects created"] --> B["each app's models module imported:
ModelBase builds Field objects and _meta"] B --> C["AppConfig.ready() runs"] C --> D["system checks (runserver, migrate, check):
fields.E###, models.E###"] end subgraph request["Every request"] E["URLconf resolves a view"] --> F["view builds a QuerySet (lazy)"] F --> G["QuerySet evaluated: Model.from_db()
creates one instance per row"] G --> H["view or template reads fields,
calls methods, get_absolute_url()"] H --> I["save() / delete(): signals, pre_save(),
get_db_prep_save(), INSERT / UPDATE / DELETE"] end D --> E
-
Start-up. Django imports every app’s
modelsmodule (stage 2 ofdjango.setup()), theModelBasemetaclass turns each class attribute that is aFieldinto an entry ofModel._meta, and only then doesAppConfig.ready()run. This is why model modules must not do I/O and whyready()is the place for signal wiring. The system check framework then validates every model (thefields.Eandmodels.Echecks; an example is shown below) beforerunserverormigrateproceeds. -
Request. Models never see the HTTP request. A view builds a lazy QuerySet, evaluating it creates model instances through
Model.from_db(), and templates and serializers read attributes and call methods. Persistence happens insave()anddelete(). WithATOMIC_REQUESTS = Truethe whole view runs in one transaction. -
Commands and tests.
migrate,loaddata,shelland the test runner use the same model classes without any request. Code that must hold no matter how a row is written (a price is never negative) belongs in the database as a constraint, not in a view.
Source: Django: Applications and Django: Models
Defining a model
A model is a Python class that subclasses django.db.models.Model. Each class attribute that is a Field instance
becomes a database column; Django derives the table name from the app label and the lower-cased class name
(catalog_book), adds an id primary key unless you declare one, and gives you a manager (objects), a constructor,
save(), delete(), validation and introspection for free.
Two small supporting modules come first: a validator and the custom field the Bookshelf Book model uses (the field
itself is explained in Custom model fields).
from django.core.exceptions import ValidationError
def validate_isbn13(value):
"""Reject anything that is not a well-formed ISBN-13 (hyphens allowed)."""
digits = value.replace("-", "")
if len(digits) != 13 or not digits.isdigit():
raise ValidationError("An ISBN-13 has exactly 13 digits.", code="invalid_isbn")
total = sum(int(d) * (3 if i % 2 else 1) for i, d in enumerate(digits[:12]))
if (10 - total % 10) % 10 != int(digits[12]):
raise ValidationError("The ISBN-13 check digit is wrong.", code="invalid_isbn")
Source: Django: Validators
class Author(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
born = models.DateField(null=True, blank=True)
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
orcid = models.CharField(max_length=19, unique=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
@classmethod
def from_db(cls, db, field_names, values, **kwargs):
instance = super().from_db(db, field_names, values, **kwargs)
# remember what was loaded; __dict__ is safe even if "name" is deferred
instance._loaded_name = instance.__dict__.get("name")
return instance
@property
def renamed(self):
loaded = getattr(self, "_loaded_name", None)
return loaded is not None and self.__dict__.get("name") != loaded
def get_absolute_url(self):
return reverse("catalog:author-detail", kwargs={"slug": self.slug})
Source: Django: Models
class Book(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
OUT_OF_PRINT = "out_of_print", "Out of print"
class Binding(models.IntegerChoices):
PAPERBACK = 1, "Paperback"
HARDCOVER = 2, "Hardcover"
EBOOK = 3, "E-book"
public_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
title = models.CharField(max_length=300, help_text="As printed on the cover.")
slug = models.SlugField(max_length=300, unique=True, blank=True)
author = models.ForeignKey(Author, on_delete=models.PROTECT, related_name="books")
isbn = models.CharField(
"ISBN",
max_length=17,
unique=True,
validators=[validate_isbn13],
db_comment="ISBN-13, with or without hyphens",
)
status = models.CharField(max_length=20, choices=Status, default=Status.DRAFT)
binding = models.PositiveSmallIntegerField(
choices=Binding, default=Binding.PAPERBACK
)
price = models.DecimalField(
max_digits=8, decimal_places=2, validators=[MinValueValidator(0)]
)
pages = models.PositiveSmallIntegerField(null=True, blank=True)
published_on = models.DateField(null=True, blank=True)
tags = TagListField()
metadata = models.JSONField(default=dict, blank=True)
stock = models.PositiveIntegerField(db_default=0)
created_at = models.DateTimeField(db_default=Now(), editable=False)
updated_at = models.DateTimeField(auto_now=True)
title_lower = models.GeneratedField(
expression=Lower("title"),
output_field=models.CharField(max_length=300),
db_persist=True,
)
cover = models.FileField(upload_to="covers/%Y/%m/", blank=True)
class Meta:
ordering = ["-published_on", "title"]
get_latest_by = "published_on"
db_table_comment = "Catalogue of books"
indexes = [
models.Index(fields=["status", "-published_on"], name="book_status_pub_idx"),
]
constraints = [
models.CheckConstraint(
condition=Q(price__gte=0), name="book_price_gte_0"
),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("catalog:book-detail", kwargs={"slug": self.slug})
@property
def is_published(self):
return self.status == self.Status.PUBLISHED
def clean(self):
if self.status == self.Status.PUBLISHED and self.published_on is None:
raise ValidationError(
{"published_on": "A published book needs a publication date."}
)
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
update_fields = kwargs.get("update_fields")
if update_fields is not None:
kwargs["update_fields"] = {*update_fields, "slug"}
super().save(**kwargs)
def delete(self, *args, **kwargs):
cover = self.cover
result = super().delete(*args, **kwargs)
if cover:
transaction.on_commit(lambda: cover.storage.delete(cover.name))
return result
Source: Django: Model field reference
The imports at the top of models.py are:
import uuid
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.db.models.functions import Lower, Now
from django.urls import reverse
from django.utils.text import slugify
from .fields import TagListField
from .validators import validate_isbn13
Source: Django: Models - quick example
After startapp catalog, the app is added to INSTALLED_APPS and a one-line include() wires the get_absolute_url()
targets into the root URLconf (the views behind them are placeholders that return a string):
INSTALLED_APPS = [
# ... the default Django apps ...
"catalog",
]
urlpatterns = [
path("admin/", admin.site.urls),
path("catalog/", include("catalog.urls")),
]
Source: Django: Applications
Then makemigrations writes the table definition and
sqlmigrate shows the SQL for the database in use (SQLite here; PostgreSQL emits uuid, jsonb, an identity column and
COMMENT ON statements instead):
$ python manage.py makemigrations catalog
Migrations for 'catalog':
catalog/migrations/0001_initial.py
+ Create model Author
+ Create model Book
$ python manage.py sqlmigrate catalog 0001
CREATE TABLE "catalog_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "public_id" char(32) NOT NULL UNIQUE,
"title" varchar(300) NOT NULL, "slug" varchar(300) NOT NULL UNIQUE, "isbn" varchar(17) NOT NULL UNIQUE,
"status" varchar(20) NOT NULL, "binding" smallint unsigned NOT NULL CHECK ("binding" >= 0),
"price" decimal NOT NULL, "pages" smallint unsigned NULL CHECK ("pages" >= 0), "published_on" date NULL,
"tags" text NOT NULL, "metadata" text NOT NULL CHECK ((JSON_VALID("metadata") OR "metadata" IS NULL)),
"stock" integer unsigned DEFAULT 0 NOT NULL CHECK ("stock" >= 0),
"created_at" datetime DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL, "updated_at" datetime NOT NULL,
"title_lower" varchar(300) GENERATED ALWAYS AS (LOWER("title")) STORED, "cover" varchar(100) NOT NULL,
"author_id" bigint NOT NULL REFERENCES "catalog_author" ("id") DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT "book_price_gte_0" CHECK ("price" >= '0'));
CREATE INDEX "catalog_book_author_id_b7811fba" ON "catalog_book" ("author_id");
CREATE INDEX "book_status_pub_idx" ON "catalog_book" ("status", "published_on" DESC);
Source: Django: Migrations
manage.py check prints no issues on PostgreSQL. On SQLite it reports two warnings for the comment options
(fields.W163 for db_comment, models.W046 for db_table_comment) because SQLite cannot store comments; the scratch
project silences exactly those two ids with SILENCED_SYSTEM_CHECKS, which is the right move when you develop on SQLite
and deploy on PostgreSQL. Exercise the model from a shell to see the lifecycle (output trimmed):
>>> from decimal import Decimal
>>> from catalog.models import Author, Book
>>> author = Author.objects.create(name="Ursula K. Le Guin", slug="ursula-k-le-guin")
>>> book = Book(title="The Dispossessed", author=author, isbn="978-0-306-40615-7",
... price=Decimal("9.99"), tags=["sf", "classic"])
>>> book.pk, repr(book.slug), type(book.stock).__name__
(None, "''", 'DatabaseDefault')
>>> book.save()
>>> book.pk, book.slug, book.stock, book.title_lower
(1, 'the-dispossessed', 0, 'the dispossessed')
>>> book.refresh_from_db()
>>> book.tags, book.status, book.get_status_display(), book.get_absolute_url()
(['sf', 'classic'], 'draft', 'Draft', '/catalog/books/the-dispossessed/')
With the development server running, the URL that get_absolute_url() produced answers (the scratch project serves it
through runserver; the body is the placeholder view’s text):
$ curl -s -w '\n%{http_code}\n' http://127.0.0.1:8000/catalog/books/the-dispossessed/
book the-dispossessed
200
Source: Django: Model instance reference
The same lifecycle is covered by the automated tests, which are the real contract of the model. Run them with:
$ python manage.py test catalog
Found 54 test(s).
System check identified no issues (2 silenced).
Ran 54 tests in 0.088s
OK
Field types
Every field class maps a Python type to a column type, validates the value, and supplies a default form field. All of them
live in django.db.models (the convention is from django.db import models and models.CharField).
| Field | Python type | Notes |
|---|---|---|
|
|
Auto-incrementing primary keys. |
|
|
64-, 32- and 16-bit. Values from -2147483648 to 2147483647 are safe in every database for |
|
|
Non-negative (0 is accepted); the database gets a |
|
|
Binary floating point; do not use it for money. |
|
|
Exact decimals. Both arguments are required on MySQL; since 6.1 they are optional on PostgreSQL, SQLite and Oracle. |
|
|
Defaults to |
|
|
Short text. |
|
|
Unbounded text. A |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Not editable by default; storing files in the database is almost always the wrong design. Since 6.1 it validates Base64 input strictly. |
|
|
Supported on every database Django supports (SQLite needs the JSON1 extension). Since 3.1; see JSONField. |
|
|
A |
|
|
Choices limited to files in a directory; |
|
|
IPv4 or IPv6; blank values are stored as |
|
the |
A column the database computes from other columns (5.0). See db_default, GeneratedField and db_comment. |
|
|
A virtual field that makes several columns the primary key (5.2). See Constraints, Indexes & Composite Keys. |
|
model instance / manager |
Relationships; see Relationships. |
Fields that only survive for old migrations raise a system check error if you use them in a model now:
NullBooleanField (fields.E903), IPAddressField (fields.E900) and CommaSeparatedIntegerField (fields.E901).
Field names have four restrictions: not a Python reserved word, no double underscore (it is the lookup separator),
no trailing underscore, and not check (it would hide Model.check()). SQL reserved words such as order or select
are fine because Django quotes every identifier. The system check catches the first three:
class Bad(models.Model):
foo__bar = models.IntegerField()
trailing_ = models.IntegerField()
class Meta:
app_label = "catalog"
fields.E002: Field names must not contain "__".
fields.E001: Field names must not end with an underscore.
Field options
All options are keyword arguments available on every field type (the first positional argument of most fields is the
human-readable verbose_name, which is why models.CharField("ISBN", max_length=17) works).
| Option | What it does |
|---|---|
|
Store empty values as |
|
Validation-level: allow an empty value in forms and |
|
A mapping, an iterable of 2-tuples, a callable returning either, or an enumeration class (5.0). Enforced by validation; the form widget becomes a select box. See Choices: TextChoices and IntegerChoices. |
|
The column name, when it must differ from the attribute (a reserved word, a legacy schema). |
|
A column comment (4.2), for people who read the schema directly. Ignored (with check |
|
A database-computed default: a literal or an expression built from literals and database functions (5.0). See db_default, GeneratedField and db_comment. |
|
Create an index on the column. Prefer |
|
The tablespace of this field’s index; defaults to |
|
A value or a callable called for every new instance. Never a mutable object: use |
|
|
|
Overrides messages by key: |
|
Text shown with the form widget. It is not HTML-escaped in generated forms. |
|
Make this the primary key (implies |
|
Enforced by the database and by validation. Not allowed on |
|
Unique per date component of another date field. Enforced by validation only, not by the database. |
|
Human-readable name; derived from the attribute (underscores to spaces) when omitted. |
|
A list of callables run by |
String fields add db_collation (not portable between databases), DateField adds auto_now and auto_now_add,
FileField adds upload_to and storage, and DecimalField adds max_digits and decimal_places. upload_to may be
a strftime pattern ("covers/%Y/%m/", used in Book) or a callable that receives the instance and the original file
name:
def cover_path(instance, filename):
return f"covers/{instance.public_id}/{filename}"
class Cover(models.Model):
public_id = models.UUIDField(default=uuid.uuid4, editable=False)
image = models.FileField(upload_to=cover_path)
class Meta:
app_label = "catalog"
Source: Django: FileField.upload_to
error_messages, validators and unique_for_date combine naturally. The test creates the table, inserts a row and checks
that both messages come back from full_clean():
from django.core.validators import RegexValidator
class Review(models.Model):
headline = models.CharField(
max_length=100,
unique_for_date="posted_on",
error_messages={"unique_for_date": "One review per headline and day."},
)
code = models.CharField(
max_length=8,
validators=[RegexValidator(r"^[A-Z]{3}-\d{4}$", "Use the form ABC-1234.")],
)
posted_on = models.DateField(default=datetime.date.today)
class Meta:
app_label = "catalog"
Source: Django: Field.error_messages
Since 6.1 DecimalField no longer needs max_digits and decimal_places on PostgreSQL, SQLite and Oracle (MySQL still
does, because it has no unconstrained numeric type):
class Measurement(models.Model):
value = models.DecimalField() # 6.1+: optional on PostgreSQL, SQLite, Oracle; MySQL still needs both
class Meta:
app_label = "catalog"
Source: Django: DecimalField
null versus blank
The two options look alike and do unrelated jobs, and mixing them up is the most common model mistake.
-
nullis about storage:null=Truelets the database column holdNULL. It changes theCREATE TABLE. -
blankis about validation:blank=Trueletsfull_clean()and everyModelFormaccept an empty value. It never touches the database.
| Setting | "" / empty form value |
None |
When to use it |
|---|---|---|---|
neither (the default) |
rejected: "This field cannot be blank." |
rejected by validation, |
A required value. |
|
accepted |
rejected for a string field (use |
Optional text ( |
|
rejected |
accepted by the database, but rejected by validation ( |
Rarely useful by itself: the database allows |
|
accepted |
accepted |
Optional non-text values ( |
The documented convention is that string-based fields (CharField, TextField) use the empty string as "no data" and
leave null=False: with null=True they would have two ways to say "nothing" (NULL and ""), and every query has to
remember both. There is one documented exception, a CharField with both unique=True and blank=True: many "" values
collide on a unique index, but many NULL values never do. Author.orcid is that case, and ModelForm cooperates by
storing None instead of "" for a null=True character field. The test below runs the full matrix on throw-away
models, shows that save() validates nothing, and demonstrates the unique-and-blank collision:
class NullBlankTests(TestCase):
@isolate_apps("catalog")
def test_the_four_combinations(self):
class Probe(models.Model):
neither = models.CharField(max_length=5)
blank_only = models.CharField(max_length=5, blank=True)
null_only = models.CharField(max_length=5, null=True)
both = models.CharField(max_length=5, null=True, blank=True)
class Meta:
app_label = "catalog"
def failing_fields(**overrides):
values = {"neither": "x", "blank_only": "x", "null_only": "x", "both": "x"}
try:
Probe(**{**values, **overrides}).full_clean()
except ValidationError as error:
return sorted(error.message_dict)
return []
self.assertEqual(failing_fields(neither=""), ["neither"])
self.assertEqual(failing_fields(neither=None), ["neither"])
self.assertEqual(failing_fields(blank_only=""), [])
self.assertEqual(failing_fields(null_only=""), ["null_only"])
self.assertEqual(failing_fields(null_only=None), ["null_only"])
self.assertEqual(failing_fields(both=None), [])
self.assertEqual(failing_fields(both=""), [])
def test_blank_is_validation_null_is_storage(self):
Author(name="x", slug="x", bio="").full_clean() # blank=True accepts ""
with self.assertRaises(ValidationError) as caught:
Author(name="", slug="x").full_clean()
self.assertEqual(caught.exception.message_dict["name"], ["This field cannot be blank."])
with self.assertRaises(IntegrityError), transaction.atomic():
Author.objects.create(name=None, slug="y") # save() validates nothing
def test_unique_and_blank_needs_null(self):
Author.objects.create(name="a", slug="")
with self.assertRaises(IntegrityError), transaction.atomic():
Author.objects.create(name="b", slug="") # "" collides with ""
Author.objects.create(name="c", slug="c", orcid=None)
Author.objects.create(name="d", slug="d", orcid=None) # NULLs never collide
self.assertEqual(Author.objects.filter(orcid__isnull=True).count(), 3)
def test_modelform_stores_none_for_null_char_fields(self):
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = ["name", "slug", "orcid"]
form = AuthorForm({"name": "n", "slug": "n", "orcid": ""})
self.assertTrue(form.is_valid())
self.assertIsNone(form.cleaned_data["orcid"])
Source: Django: Field.null and Field.blank
blank=True combined with null=False on a non-string field (an integer, say) needs a clean() override or a default
that supplies the missing value, because an empty form value cannot be stored in a NOT NULL integer column.
Choices: TextChoices and IntegerChoices
choices restricts a field to a closed set of values: validation rejects anything else (invalid_choice) and the form
widget becomes a <select>. For every field with choices Django adds get_FOO_display(), which returns the label of the
current value (or the raw value itself if it is not a valid choice).
The books of the Django 1.11 era declare choices as loose module-level tuples and compare against bare strings. Since 3.0 the idiom is an enumeration class nested in the model, which gives every value a name, a translatable label and a place to live:
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
OUT_OF_PRINT = "out_of_print", "Out of print"
class Binding(models.IntegerChoices):
PAPERBACK = 1, "Paperback"
HARDCOVER = 2, "Hardcover"
EBOOK = 3, "E-book"
Source: Django: Enumeration types
class ChoicesTests(TestCase):
def test_enumeration_api(self):
self.assertEqual(Book.Status.choices[0], ("draft", "Draft"))
self.assertEqual(Book.Status.values, ["draft", "published", "out_of_print"])
self.assertEqual(Book.Status.labels, ["Draft", "Published", "Out of print"])
self.assertEqual(Book.Status.names, ["DRAFT", "PUBLISHED", "OUT_OF_PRINT"])
self.assertEqual(Book.Status("published").label, "Published")
self.assertEqual(Book.Binding.HARDCOVER.value, 2)
self.assertEqual(Book.Status.DRAFT, "draft") # a str as well as an enum member
def test_display_and_filtering(self):
make_book(status=Book.Status.PUBLISHED, published_on=datetime.date(1974, 5, 1),
binding=Book.Binding.EBOOK)
book = Book.objects.get(status=Book.Status.PUBLISHED)
self.assertEqual(book.get_status_display(), "Published")
self.assertEqual(book.get_binding_display(), "E-book")
self.assertTrue(book.is_published)
def test_choices_are_enforced_by_validation_not_by_save(self):
book = make_book()
book.status = "bogus"
book.save() # no error: save() does not validate
with self.assertRaises(ValidationError) as caught:
book.full_clean()
self.assertEqual(caught.exception.error_dict["status"][0].code, "invalid_choice")
self.assertEqual(book.get_status_display(), "bogus") # falls back to the raw value
def test_blank_choice_label_in_6_1(self):
class StatusForm(forms.ModelForm):
class Meta:
model = Book
fields = ["status"]
self.assertNotIn("Select an option", str(StatusForm()["status"])) # has a default
@isolate_apps("catalog")
def render_without_default():
class Probe(models.Model):
kind = models.CharField(max_length=5, choices=Book.Status)
class Meta:
app_label = "catalog"
return Probe._meta.get_field("kind").formfield().widget.render("kind", None)
self.assertIn("- Select an option -", render_without_default())
Source: Django: Field.choices
TextChoices and IntegerChoices (both since 3.0) are enum.Enum subclasses with these additions: a .label property;
.choices, .labels, .values and .names class properties; enum.unique() enforced; and an optional trailing
(lazy) string in each member’s value tuple that becomes the label (when it is omitted the label is derived from the member
name, so JET_SKI becomes "Jet Ski"). Members are also real str or int values, so Book.Status.DRAFT == "draft",
they filter and serialize naturally, and migrations store the plain value.
Since 5.0 the choices argument itself accepts more than lists of tuples: you can pass the enumeration class directly
(choices=Status, as Book does, instead of choices=Status.choices), a mapping, grouped choices, or a callable that
takes no arguments and returns any of those. A callable is the way to feed choices from I/O or a third-party inventory
(currencies, countries) without evaluating it at import time. Group labels nest naturally in a mapping; enumeration types
do not support groups. The example below runs, and check confirms max_length is long enough for the longest value
(fields.E009 is raised when it is not, which is how a too-small max_length is caught):
def currencies():
return {"EUR": "Euro", "USD": "US dollar"}
MEDIA_CHOICES = {
"Audio": {"vinyl": "Vinyl", "cd": "CD"},
"Video": {"vhs": "VHS Tape", "dvd": "DVD"},
"unknown": "Unknown",
}
class Purchase(models.Model):
currency = models.CharField(max_length=3, choices=currencies)
media = models.CharField(max_length=7, choices=MEDIA_CHOICES, default="cd")
class Meta:
app_label = "catalog"
Source: Django: Field.choices
Three more details are worth knowing. __empty__ on an enumeration class sets the label of the blank option, because an
enumeration with a concrete type cannot contain a None member; the functional API (models.TextChoices("Medal", "GOLD
SILVER BRONZE")) builds an enumeration at runtime with generated labels:
class Vehicle(models.TextChoices):
CAR = "C"
JET_SKI = "J"
class Answer(models.IntegerChoices):
NO = 0, _("No")
YES = 1, _("Yes")
__empty__ = _("(Unknown)")
Medal = models.TextChoices("Medal", "GOLD SILVER BRONZE")
Source: Django: Enumeration types
What changed recently. In Django 6.1 the default label of the blank option in a select box changed from --------- to
- Select an option -; it still appears only when the field has no default or has blank=True (the test above checks
both sides of that rule). Changing the order or members of choices creates a new migration, but sqlmigrate prints
(no-op) for it: choices are validation, not schema. Adding "Upcoming" to Book.Status produced
~ Alter field status on book and this SQL:
BEGIN;
--
-- Alter field status on book
--
-- (no-op)
COMMIT;
Source: Django: django-admin sqlmigrate
JSONField
JSONField (3.1; the PostgreSQL-only django.contrib.postgres.fields.JSONField of older books was removed in 4.0) stores
anything the json module can serialize and gives it back as native Python data. PostgreSQL uses jsonb, so it can be
indexed with a GIN index; on other databases a B-tree index on the whole column is not useful. Always give it a callable
default; the system check refuses a shared mutable one:
class Broken(models.Model):
payload = models.JSONField(default={}) # one dict shared by every instance
class Meta:
app_label = "catalog"
fields.E010: JSONField default should be a callable instead of an instance so that it's not shared between all field instances.
Source: Django: System check framework
encoder and decoder let you store types the standard encoder rejects, such as dates and UUIDs; DjangoJSONEncoder is
the usual choice. The decoder has to cope with the fact that a string that looks like a date was perhaps always a string:
class Event(models.Model):
payload = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
class Meta:
app_label = "catalog"
Source: Django: JSONField
Key, index and path lookups work through the ORM on every supported database (metadata__series__name="Hainish",
metadata__awards__0="Hugo", metadata__has_key="awards", numeric comparisons such as metadata__series__no__gte=3),
and they are covered in Making Queries. The test below stores nested data and queries it.
It also shows the 6.1 JSONNull expression: SQL NULL and the JSON scalar null are different things, and JSONNull()
is the explicit way to store or query the latter. Assigning None stores SQL NULL; both come back from the database as
None, so they are hard to tell apart; isnull=True matches SQL NULL and an exact match on JSONNull() matches JSON
null. Storing JSON null does not violate null=False, and exact-matching None to mean JSON null is deprecated in 6.1
(RemovedInDjango2028Warning):
class JsonFieldTests(TestCase):
def test_round_trip_and_key_lookups(self):
make_book(metadata={"awards": ["Hugo", "Nebula"], "series": {"name": "Hainish", "no": 5}})
book = Book.objects.get()
self.assertEqual(book.metadata["series"]["no"], 5)
self.assertTrue(Book.objects.filter(metadata__has_key="awards").exists())
self.assertTrue(Book.objects.filter(metadata__series__name="Hainish").exists())
self.assertTrue(Book.objects.filter(metadata__awards__0="Hugo").exists())
self.assertTrue(Book.objects.filter(metadata__series__no__gte=3).exists())
def test_json_null_is_not_sql_null(self):
book = make_book(metadata=JSONNull()) # the JSON scalar null (6.1)
book.refresh_from_db()
self.assertIsNone(book.metadata)
self.assertTrue(Book.objects.filter(metadata=JSONNull()).exists())
self.assertFalse(Book.objects.filter(metadata__isnull=True).exists())
Source: Django: Querying JSONField
Use JSONField for data whose shape varies per row or comes from outside (an API payload, per-book extras) and keep
anything you filter, sort, join or constrain constantly in real columns: JSON values are not type-checked by the database,
cannot take a foreign key and are awkward to migrate. Oracle stores only objects and arrays, not scalars.
db_default, GeneratedField and db_comment
Python-side default is applied when an instance is created, so rows inserted by raw SQL, bulk_create from another tool
or a migration that adds a column never see it. Django 5.0 added the database-side counterparts.
-
db_defaultis a default stored in the column definition. It accepts a literal or an expression made of literals and database functions (Now(),Pi(),TruncMonth(…)) and may not reference other columns. When both are set,defaultwins in Python anddb_defaultstill protects the table. On an unsaved instance, a field withdb_defaultand no value holds aDatabaseDefaultplaceholder (shown in the shell session above), and the real value is read back from theINSERT … RETURNINGclause aftersave(). -
GeneratedFieldis a column the database computes from other columns of the same row:expression(deterministic, no references to other generated fields),output_field(a field instance that defines the type) anddb_persist(required). A stored column (True) is computed when written and occupies space; a virtual column (False) is computed when read. It cannot be assigned, has nodefaultand is not editable. PostgreSQL before 18 supports only stored columns; since 6.1 PostgreSQL 18+ also supports virtual ones and Oracle 23ai/26ai (23.7+) supports stored ones. -
db_commentandMeta.db_table_comment(4.2) put documentation into the schema for people who query the database directly.
class Square(models.Model):
side = models.IntegerField()
label = models.CharField(max_length=20, db_default="Square")
created = models.DateTimeField(db_default=Now())
circumference = models.FloatField(db_default=2 * Pi())
area = models.GeneratedField(
expression=F("side") * F("side"),
output_field=models.BigIntegerField(),
db_persist=True,
)
shout = models.GeneratedField(
expression=Lower("label"),
output_field=models.CharField(max_length=20),
db_persist=False, # virtual: computed on read (PostgreSQL 18+, SQLite, MySQL)
)
class Meta:
app_label = "catalog"
Source: Django: GeneratedField
On SQLite this is the DDL (stored and virtual generated columns, and both defaults written as SQL expressions):
CREATE TABLE "catalog_square" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "side" integer NOT NULL,
"label" varchar(20) DEFAULT 'Square' NOT NULL,
"created" datetime DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
"circumference" real DEFAULT ((2 * PI())) NOT NULL,
"area" bigint GENERATED ALWAYS AS (("side" * "side")) STORED,
"shout" varchar(20) GENERATED ALWAYS AS (LOWER("label")) VIRTUAL);
Source: Django: GeneratedField and Field.db_default
The Book model uses all three: stock and created_at come from db_default, title_lower is a stored generated
column, and isbn carries a db_comment. The INSERT Django issued for the first save() (see save() and how Django chooses INSERT or UPDATE) shows the
effect: none of those three columns is written, they appear only in the RETURNING clause, and the instance receives
their values without a second query. That is new behaviour in 6.0: before it, a GeneratedField stayed stale until
refresh_from_db(); on MySQL and MariaDB, which have no RETURNING, the field is marked deferred and loaded on first
access.
The tests that pin this behaviour down, including the 6.0 refresh of the generated column after save():
class DefaultsTests(TestCase):
def test_defaults_slug_and_database_computed_values(self):
book = make_book()
book.refresh_from_db()
self.assertEqual(book.slug, "the-dispossessed") # set by save()
self.assertEqual(book.status, Book.Status.DRAFT) # default=
self.assertEqual(book.stock, 0) # db_default=0
self.assertIsNotNone(book.created_at) # db_default=Now()
self.assertEqual(book.title_lower, "the dispossessed") # GeneratedField
self.assertEqual(book.tags, []) # callable default
self.assertEqual(book.metadata, {})
def test_unsaved_instance_holds_a_database_default_placeholder(self):
book = Book(title="t")
self.assertIsInstance(book.stock, DatabaseDefault)
self.assertFalse(book._is_pk_set())
def test_generated_field_is_refreshed_and_read_only(self):
book = make_book()
self.assertEqual(book.title_lower, "the dispossessed") # no refresh needed (6.0+)
book.title = "THE LEFT HAND OF DARKNESS"
book.save()
self.assertEqual(book.title_lower, "the left hand of darkness")
book.title_lower = "ignored" # the database owns this column
book.save()
book.refresh_from_db()
self.assertEqual(book.title_lower, "the left hand of darkness")
def test_auto_now_only_runs_for_fields_that_are_saved(self):
book = make_book()
first = book.updated_at
book.pages = 400
book.save()
self.assertGreater(book.updated_at, first)
stamp = Book.objects.get().updated_at
book.pages = 401
book.save(update_fields=["pages"]) # updated_at not listed: not touched
Book.objects.update(pages=5) # update() never calls pre_save()
self.assertEqual(Book.objects.get().updated_at, stamp)
Source: Django: Field.db_default
Generated columns are the right tool for a derived value you want to index or filter on in the database (a lower-cased
title for case-insensitive search, a full-text tsvector on PostgreSQL). Check the restrictions of your database: for
example PostgreSQL requires every function in the expression to be IMMUTABLE, and Django does not validate that.
Since 5.2 Model.validate_constraints() can validate constraints that involve a GeneratedField.
Primary keys
If no field has primary_key=True (and no CompositePrimaryKey is declared), Django adds
id = models.BigAutoField(primary_key=True, …). The type comes from AppConfig.default_auto_field for the app, or
globally from the DEFAULT_AUTO_FIELD setting. Two version facts matter for projects of different ages:
-
DEFAULT_AUTO_FIELDwas introduced in 3.2. From then until 5.2 thestartprojectandstartapptemplates wroteBigAutoFieldexplicitly, but the fallback default stayedAutoField; 6.0 madeBigAutoFieldthe fallback and removed those two lines from the templates (which is why the project generated by 6.1 has neither). A project that never set the value and was created before 3.2 should pinDEFAULT_AUTO_FIELD = "django.db.models.AutoField"(ordefault_auto_fieldin the app config) to keep its existing 32-bit keys and avoid a migration that rewrites every key column. -
The books predate the setting entirely and show bare
idfields of typeAutoField; do not copy theirAutoFieldforeign-key columns into new tables if you can avoid it, because running out of 32-bit ids on a busy table is painful to fix.
Every model has a pk property that aliases the primary-key field or fields. The primary key is read-only in the sense
that changing it on an existing instance and saving creates a new row; delete() sets it back to None. When the
public identifier of a row appears in URLs, an extra UUIDField (as Book.public_id) hides the sequence; making the
UUIDField the primary key is also fine, and because it has a default, Django knows a fresh instance is an INSERT (the
test counts the queries):
class Edition(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
label = models.CharField(max_length=50)
class Meta:
app_label = "catalog"
Source: Django: UUIDField
Since 5.2 a table can have a composite primary key, declared as a virtual pk field. Foreign keys to a model with a
composite key are not supported and pk is then a tuple; the topic is detailed in
Constraints, Indexes & Composite Keys:
class Release(models.Model):
pk = models.CompositePrimaryKey("version", "name")
version = models.IntegerField()
name = models.CharField(max_length=20)
class Meta:
app_label = "catalog"
Source: Django: Composite primary keys
Validation: what runs and when
A model has four validation steps, and save() runs none of them. They are called in this order by
Model.full_clean(exclude=None, validate_unique=True, validate_constraints=True), which collects every error into one
ValidationError whose message_dict is keyed by field name (or __all__ for errors about the whole model):
-
clean_fields()converts each field withto_python()and runs its validators,choices,nullandblankchecks; -
clean()is your hook for rules that involve several fields (seeBook.clean()above); -
validate_unique()checksunique,unique_for_date/month/yearandunique_togetherwith a query; -
validate_constraints()(4.1) evaluatesMeta.constraintsin Python.
A ModelForm calls the same steps for the fields it contains when is_valid() runs, which is why a form shows Book’s
messages with no extra code; forms are covered in Forms. Outside forms (a management command, an
importer, a REST view that bypasses forms) you must call `full_clean() yourself before save(), or rely on constraints.
class ValidationTests(TestCase):
def test_validators(self):
validate_isbn13(ISBN)
validate_isbn13("9780306406157")
book = Book(title="t", isbn="978-0-306-40615-8", price=Decimal("-1"), author=make_author())
with self.assertRaises(ValidationError) as caught:
book.full_clean(exclude=["slug"])
self.assertEqual(caught.exception.error_dict["isbn"][0].code, "invalid_isbn")
self.assertIn("price", caught.exception.message_dict) # MinValueValidator(0)
def test_clean_reports_a_field_error(self):
book = make_book(status=Book.Status.PUBLISHED)
with self.assertRaises(ValidationError) as caught:
book.full_clean()
self.assertEqual(
caught.exception.message_dict,
{"published_on": ["A published book needs a publication date."]},
)
book.published_on = datetime.date(1974, 5, 1)
book.full_clean()
def test_validate_unique_is_part_of_full_clean(self):
make_book()
duplicate = Book(title="Other", slug="other", isbn=ISBN, price=1, author=Author.objects.get())
with self.assertRaises(ValidationError) as caught:
duplicate.full_clean()
self.assertEqual(caught.exception.message_dict["isbn"], ["Book with this ISBN already exists."])
def test_check_constraint_in_python_and_in_the_database(self):
book = make_book()
book.price = Decimal("-5")
with self.assertRaises(ValidationError):
book.validate_constraints()
with self.assertRaises(IntegrityError), transaction.atomic():
Book.objects.filter(pk=book.pk).update(price=-5) # update() skips Python checks
Notes on writing validation code:
-
A validator is any callable that raises
ValidationErrorfor a bad value. Give the error acodeso callers and tests can branch on it (invalid_isbnabove), and keep validators importable at module level so migrations can serialize them. A lambda cannot be serialized. -
In
clean(), raiseValidationError({"field": "message"})to attach an error to a field; a bare message goes underNON_FIELD_ERRORS. A dictionary that names a field excluded from aModelFormraisesValueError; overrideclean_fields(exclude=None)instead when the form may omit the field. -
unique=Trueis checked twice: by validation (a friendly message, a race is possible between the check and the insert) and by the database (the authoritativeIntegrityError). CatchIntegrityErroraround the write when two requests can create the same value concurrently. -
A
CheckConstraintis also checked twice. The test showsvalidate_constraints()rejecting a negative price in Python and the database rejecting a bulkupdate()that bypasses Python.violation_error_code(5.0) andviolation_error_messagecustomise the error. The syntax ofCheckConstraint(condition=…)changed in 5.1 (thecheckargument was deprecated then and removed in 6.0). -
full_clean()andvalidate_unique()run queries, so they are synchronous code: see Sync and async.
The Meta options
Anything that is about the table or the model as a whole, rather than one column, goes in the inner class Meta.
Book uses ordering, get_latest_by, db_table_comment, indexes and constraints. A model that omits Meta gets
all the defaults.
| Option | Effect |
|---|---|
|
The class is a base for other models and has no table. See Model Inheritance & the Meta API. |
|
Required when a model is defined outside a package listed in |
|
Which manager supplies |
|
The table name; use lower case, especially on MySQL. Derived as |
|
A table comment (4.2); |
|
The tablespace for the table (default |
|
The default reverse accessor name (otherwise |
|
The field(s) used by |
|
|
|
Makes rows orderable within a parent (adds an |
|
The default |
|
Extra permissions, and the list of automatic ones ( |
|
A subclass that adds Python behaviour but shares the parent’s table. |
|
Skip the model in migrations unless the connection has the feature or is that vendor ( |
|
Use the pre-1.6 |
|
A list of |
|
A list of |
|
Sets of columns that must be unique together. The docs recommend |
|
Human-readable names (a munged |
|
Read-only: |
Source: Django: Model Meta options
An example that uses several of the less common options on isolated models (each one passes check):
class Book(models.Model):
title = models.CharField(max_length=300)
class Meta:
app_label = "catalog"
class Edition(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
number = models.PositiveSmallIntegerField()
released = models.DateField(null=True)
class Meta:
app_label = "catalog"
db_table = "catalog_edition"
verbose_name = "edition"
verbose_name_plural = "editions"
ordering = [F("released").desc(nulls_last=True), "number"]
default_related_name = "editions"
permissions = [("can_reprint", "Can reprint an edition")]
default_permissions = ("add", "change", "view")
constraints = [
models.UniqueConstraint(fields=["book", "number"], name="edition_book_number_uniq"),
]
Source: Django: default_related_name
managed = False is for tables created by someone else; migrations leave them alone, and you must create them yourself in
tests. A proxy model shares its parent’s table and only changes Python-level behaviour (default ordering, methods,
managers):
class LegacyBook(models.Model):
isbn = models.CharField(max_length=17, primary_key=True)
title = models.CharField(max_length=300)
class Meta:
app_label = "catalog"
managed = False # a table or view created by someone else
db_table = "legacy_books"
class Hardback(LegacyBook):
class Meta:
app_label = "catalog"
proxy = True
ordering = ["title"]
Source: Django: Meta.managed and Proxy models
order_with_respect_to replaces a hand-written position column. It adds _order, makes it the default ordering, and
gives the parent get_chapter_order() / set_chapter_order(); the test reorders three chapters:
class Novel(models.Model):
title = models.CharField(max_length=300)
class Meta:
app_label = "catalog"
class Chapter(models.Model):
novel = models.ForeignKey(Novel, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
class Meta:
app_label = "catalog"
order_with_respect_to = "novel"
unique_together still works and still raises a ValidationError with the code unique_together, but new code should
declare a UniqueConstraint (it can be conditional, deferrable, functional):
class Seat(models.Model):
row = models.CharField(max_length=2)
number = models.PositiveSmallIntegerField()
class Meta:
app_label = "catalog"
unique_together = [["row", "number"]] # older spelling, still supported
Source: Django: Meta.unique_together
Three details bite in practice. Meta.ordering costs something on every unordered query and every foreign key adds its
own ordering to joins; it is ignored in GROUP BY queries (those built with values() and annotate()); and a query with
no ordering returns rows in an unspecified order, so only order by a set of fields that identify each row uniquely when you
need a guarantee. Meta.index_together is gone: it was deprecated in 4.2 in favour of indexes and removed in 5.1
(the books that use it need a one-line change to models.Index(fields=[…])). Finally, Meta is inherited from abstract
bases and partly from multi-table parents, which is the subject of the inheritance page. The class-level _meta object
(not the Meta you write) exposes everything at runtime: Book._meta.get_fields(), get_field("isbn"), label,
db_table, pk and the field flags concrete, is_relation and many_to_one. The test below reads them back:
class MetaTests(TestCase):
def test_meta_options(self):
opts = Book._meta
self.assertEqual(opts.db_table, "catalog_book")
self.assertEqual(opts.label, "catalog.Book")
self.assertEqual(opts.ordering, ["-published_on", "title"])
self.assertEqual(opts.get_latest_by, "published_on")
self.assertEqual(opts.get_field("isbn").verbose_name, "ISBN")
self.assertEqual(opts.get_field("isbn").db_comment, "ISBN-13, with or without hyphens")
self.assertIn("title_lower", [field.name for field in opts.get_fields()])
self.assertEqual(Book.objects.all().query.order_by, ()) # Meta.ordering applies at compile time
Source: Django: Model _meta API
Model methods
Methods on the model class carry behaviour that concerns a single row. The ones every model should consider are below;
remember that all of them are plain Python and run in memory, so they must not query the database casually (an __str__
that follows a foreign key costs one query per row when the admin lists the model).
__str__
__str__() returns the human-readable label of an instance. Django uses it in the admin, when a template prints an
object, in the shell and in error messages, so always define it. Book.__str__ returns the title. The Python 2 era idioms
in the books (__unicode__, @python_2_unicode_compatible) are gone: six and python_2_unicode_compatible were removed in
Django 3.0, and __str__ is all you write.
get_absolute_url
get_absolute_url() returns the canonical path of an object. It is used by the admin ("View on site"), the sitemap and
feed frameworks and by your templates, so a URL change happens in one place.
Build it with reverse() from a validated field such as a slug, never from raw text (a name of /example.com would
produce a protocol-relative URL), and return ASCII, URL-encoded text. The @models.permalink decorator of older books was
removed in Django 2.1. The tests resolve it through the real URLconf and use it from a template, which is the place to use
it instead of hard-coding paths:
def test_absolute_url(self):
book = make_book()
self.assertEqual(book.get_absolute_url(), "/catalog/books/the-dispossessed/")
self.assertEqual(self.client.get(book.get_absolute_url()).content, b"book the-dispossessed")
self.assertEqual(book.author.get_absolute_url(), "/catalog/authors/ursula-k-le-guin/")
def test_template_links_to_the_canonical_url(self):
template = Template('<a href="{{ book.get_absolute_url }}">{{ book }}</a>')
html = template.render(Context({"book": make_book()}))
self.assertEqual(html, '<a href="/catalog/books/the-dispossessed/">The Dispossessed</a>')
from django.urls import path
from . import views
app_name = "catalog"
urlpatterns = [
path("authors/<slug:slug>/", views.author_detail, name="author-detail"),
path("books/<slug:slug>/", views.book_detail, name="book-detail"),
]
Source: Django: get_absolute_url()
save() and how Django chooses INSERT or UPDATE
save() does five things: sends pre_save, asks every field to pre_save() (this is where auto_now and
auto_now_add are filled), converts each value with get_db_prep_save(), runs the INSERT or UPDATE, and sends
post_save. The algorithm for the statement is:
-
If the primary key is unset (
None, or aDatabaseDefault), Django issues anINSERT. -
If it is set, Django issues an
UPDATE, and falls back to anINSERTwhen no row matched. -
If the primary key field has a
defaultordb_default(aUUIDFieldwithdefault=uuid.uuid4), a new instance isINSERT`ed directly and only an existing instance is `UPDATE`d, so the extra `UPDATEattempt disappears.
Setting an explicit primary key that already exists therefore silently overwrites that row. force_insert=True and
force_update=True (mutually exclusive) override the choice; force_insert can also take a tuple of parent classes under
multi-table inheritance. Since 6.0 a forced update that matches no row raises Model.NotUpdated instead of a generic
DatabaseError. Positional arguments to save() were deprecated in 5.1 and removed in 6.0, so obj.save(True) is now an
error: always write save(force_insert=True).
update_fields restricts the UPDATE to the named columns (and forces an update). The statements Django actually sent for
Book(…).save(), then save(update_fields=["pages"]), then a plain save() after changing the price (SQLite, trimmed):
INSERT INTO "catalog_book" ("public_id", "title", "slug", "author_id", "isbn", "status", "binding", "price", "pages",
"published_on", "tags", "metadata", "updated_at", "cover") VALUES ('55c6...', 'The Dispossessed', 'the-dispossessed', 1,
'978-0-306-40615-7', 'draft', 1, '9.99', NULL, NULL, '', '{}', '2026-10-02 23:14:25.341029', '')
RETURNING "catalog_book"."id", "catalog_book"."stock", "catalog_book"."created_at", "catalog_book"."title_lower"
UPDATE "catalog_book" SET "pages" = 387 WHERE "catalog_book"."id" = 1
UPDATE "catalog_book" SET "public_id" = '55c6...', "title" = 'The Dispossessed', ... "price" = '10.99', "pages" = 387, ...,
"updated_at" = '2026-10-02 23:14:25.341372', "cover" = '' WHERE "catalog_book"."id" = 1 RETURNING "catalog_book"."title_lower"
Notice what is absent from the INSERT: stock, created_at and title_lower come from the database and are read back
through RETURNING. Notice also that the second statement writes one column and does not touch updated_at.
Overriding save() is how Book fills its slug. Three rules from the docs apply to every override: call super().save(),
pass the keyword arguments through (**kwargs, so a future Django argument keeps working), and if you change a field
inside save() add it to update_fields when that argument was given, or the change is silently dropped. The test below
fails if the last rule is broken, and the others show the other save() behaviours described here:
class SaveTests(TestCase):
def test_primary_key_state_and_equality(self):
book = Book(title="t", isbn=ISBN, price=1, author=make_author())
self.assertTrue(book._state.adding)
self.assertIsNone(book.pk)
with self.assertRaises(TypeError):
hash(book)
book.save()
self.assertFalse(book._state.adding)
self.assertEqual(book.pk, book.id)
self.assertEqual(Book.objects.get(pk=book.pk), book)
self.assertEqual(Book(pk=1), Book(pk=1))
self.assertNotEqual(Book(pk=1), Author(pk=1))
self.assertNotEqual(Book(), Book()) # unsaved instances equal only themselves
def test_assigning_does_not_convert_types(self):
book = make_book()
book.price = "12.50"
self.assertEqual(book.price, "12.50") # still a str in memory
book.save() # the database layer converts while saving...
self.assertEqual(book.price, "12.50") # ...but the instance keeps what you assigned
book.refresh_from_db()
self.assertEqual(book.price, Decimal("12.50"))
book.price = "abc"
with self.assertRaises(ValidationError):
book.full_clean() # validation is where conversion (and its errors) happen
def test_copy_a_row(self):
book = make_book()
book.pk = None
book._state.adding = True
book.isbn, book.slug, book.public_id = "9780140449136", "copy", uuid.uuid4()
book.save()
self.assertEqual(Book.objects.count(), 2)
def test_update_fields_and_refresh_from_db(self):
book = make_book()
book.title, book.pages = "X", 1
book.save(update_fields=["pages"])
book.refresh_from_db()
self.assertEqual((book.title, book.pages), ("The Dispossessed", 1))
book.title = "Y"
book.refresh_from_db(fields=["pages"])
self.assertEqual(book.title, "Y") # only "pages" was reloaded
def test_override_adds_slug_to_update_fields(self):
book = make_book()
again = Book.objects.get(pk=book.pk)
again.slug = ""
again.save(update_fields=["title"])
self.assertEqual(Book.objects.get(pk=book.pk).slug, "the-dispossessed")
def test_forced_update_of_a_missing_row_raises_not_updated(self):
ghost = Book(pk=99999, title="z", isbn="1", price=1, author=make_author())
with self.assertRaises(Book.NotUpdated):
ghost.save(force_update=True)
def test_f_expression_avoids_lost_updates(self):
book = make_book(stock=5)
Book.objects.filter(pk=book.pk).update(stock=models.F("stock") + 1)
book.stock = models.F("stock") + 1
book.save()
book.refresh_from_db()
self.assertEqual(book.stock, 7)
def test_deferred_fields_load_on_access(self):
make_book()
book = Book.objects.only("title").get()
self.assertIn("isbn", book.get_deferred_fields())
with CaptureQueriesContext(connection) as queries:
book.isbn
self.assertEqual(len(queries), 1)
def test_get_next_by_needs_a_non_null_date_field(self):
author = make_author()
first = make_book(author)
second = make_book(author, title="B", isbn="9780140449136")
self.assertEqual(first.get_next_by_updated_at(), second)
self.assertEqual(second.get_previous_by_updated_at(), first)
self.assertFalse(hasattr(first, "get_next_by_published_on")) # null=True
def test_from_db_tracks_a_rename(self):
make_author()
author = Author.objects.get()
self.assertFalse(author.renamed)
author.name = "Ursula Le Guin"
self.assertTrue(author.renamed)
self.assertFalse(Author.objects.only("slug").get().renamed) # name deferred: no crash
def test_absolute_url(self):
book = make_book()
self.assertEqual(book.get_absolute_url(), "/catalog/books/the-dispossessed/")
self.assertEqual(self.client.get(book.get_absolute_url()).content, b"book the-dispossessed")
self.assertEqual(book.author.get_absolute_url(), "/catalog/authors/ursula-k-le-guin/")
def test_template_links_to_the_canonical_url(self):
template = Template('<a href="{{ book.get_absolute_url }}">{{ book }}</a>')
html = template.render(Context({"book": make_book()}))
self.assertEqual(html, '<a href="/catalog/books/the-dispossessed/">The Dispossessed</a>')
def test_cover_file_is_removed_after_commit(self):
book = make_book()
book.cover.save("cover.png", ContentFile(b"x"), save=True)
path = book.cover.path
self.assertTrue(os.path.exists(path))
self.assertRegex(book.cover.name, r"^covers/\d{4}/\d{2}/cover")
with self.captureOnCommitCallbacks(execute=True):
book.delete()
self.assertFalse(os.path.exists(path))
Assigning an F() expression (book.stock = F("stock") + 1; book.save()) or calling update() with one moves the
arithmetic into the database and avoids the lost-update race of read, add one in Python, write back. auto_now fields
are refreshed only when pre_save() runs for them, which is why save(update_fields=["pages"]) and QuerySet.update()
leave updated_at alone (DefaultsTests.test_auto_now_only_runs_for_fields_that_are_saved proves it).
delete()
delete() issues the DELETE for the row (and cascades according to each relation’s on_delete), returns the number of
rows deleted with a dictionary per model, and sets pk back to None while leaving the other attributes in memory. In 6.1
the DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT options of on_delete let the database do the cascade; the return
value then counts only the model you deleted, and pre_delete / post_delete are not sent for the cascaded rows. They are
covered in Relationships.
Overriding delete() is the classic place to clean up side effects, with one big limit: it is not called by
QuerySet.delete() or by a cascade from a parent. If the cleanup must always run, use the pre_delete / post_delete
signals (Signals), and defer irreversible work such as deleting a file until the transaction
commits. Django never removes files for you when a row is deleted (the field reference says so), and removing one
immediately would lose it if the surrounding transaction rolled back, so Book.delete() above schedules the removal with
transaction.on_commit(). The test attaches a cover, deletes the book and runs the
commit callbacks:
def test_cover_file_is_removed_after_commit(self):
book = make_book()
book.cover.save("cover.png", ContentFile(b"x"), save=True)
path = book.cover.path
self.assertTrue(os.path.exists(path))
self.assertRegex(book.cover.name, r"^covers/\d{4}/\d{2}/cover")
with self.captureOnCommitCallbacks(execute=True):
book.delete()
self.assertFalse(os.path.exists(path))
from_db(), refresh_from_db() and deferred fields
Model.from_db() is the class method the ORM calls to build an instance from a row; override it when an instance should
remember how it looked when loaded, which makes "has this field changed" checks cheap. The Author model records its
name:
@classmethod
def from_db(cls, db, field_names, values, **kwargs):
instance = super().from_db(db, field_names, values, **kwargs)
# remember what was loaded; __dict__ is safe even if "name" is deferred
instance._loaded_name = instance.__dict__.get("name")
return instance
Two details of the signature. Django 6.1 added a fetch_mode keyword argument (the fetch mode of the query that loaded
the instance, see Query Optimization & Fetch Modes) and deprecated
overrides that do not accept it (they stop working in Django 2028), so forward **kwargs to super().from_db() as above
instead of copying the documented full implementation. And read __dict__ rather than the attribute when a field may be
deferred, because attribute access on a deferred field runs a query.
refresh_from_db(using=None, fields=None, from_queryset=None) reloads field values from the database and clears cached
relations; call it after an F() update, after update(), or inside tests. fields=[…] reloads only those fields
(the unlisted ones keep your unsaved edits, as the test above shows); from_queryset (5.1) lets you lock the row with
select_for_update(), reload through a soft-delete manager, or cache related objects with select_related(). It does not
reload annotations and does not clear cached_property values. get_deferred_fields() lists the fields an only() or
defer() query left out; touching one runs a single-row query (a behaviour that 6.1’s FETCH_PEERS mode can batch).
Identity, state and the generated methods
-
pkaliases the primary key (a tuple for a composite key).Model.__eq__compares the concrete class and the primary key (a proxy equals its parent; a multi-table child does not), and instances with no primary key equal only themselves.__hash__ishash(pk)and raisesTypeErrorfor an unsaved instance, so an unsaved model cannot go in a set or be a dict key. -
_state.addingisTrueuntil the instance is saved or loaded,_state.dbis the alias it was loaded from and, since 6.1,_state.fetch_modeis the fetch mode used for later field loads._is_pk_set()tells whether the key is set; aDatabaseDefaultcounts as unset since 6.1. -
For a field with
choicesDjango addsget_FOO_display(); for every non-nullDateFieldorDateTimeFieldit addsget_next_by_FOO()andget_previous_by_FOO()(with the primary key as tie-breaker, so they need a saved instance). The test above shows they are absent for the nullablepublished_on. -
To duplicate a row, set
pk = Noneand_state.adding = True(and reset unique values), thensave(). -
Assigning a value does not convert it.
book.price = "12.50"leaves astrin memory until you reload or validate; the conversion (and its errors) happens infull_clean()and in the database layer, not on assignment:
def test_assigning_does_not_convert_types(self):
book = make_book()
book.price = "12.50"
self.assertEqual(book.price, "12.50") # still a str in memory
book.save() # the database layer converts while saving...
self.assertEqual(book.price, "12.50") # ...but the instance keeps what you assigned
book.refresh_from_db()
self.assertEqual(book.price, Decimal("12.50"))
book.price = "abc"
with self.assertRaises(ValidationError):
book.full_clean() # validation is where conversion (and its errors) happen
-
Pickled models are valid only for the Django version that wrote them (a
RuntimeWarningis raised on a mismatch), so do not use pickles as long-term storage. -
Do not override
__init__(); the signature must stay as Django calls it. Put construction logic in a class method or in a manager method (Book.objects.create_book(…)), which is the approach the docs prefer.
Sync and async
Models are plain Python objects, so reading attributes is safe anywhere. Anything that touches the database is not.
Since 4.1 the QuerySet API has async variants (aget(), acreate(), acount(), async for), and since 4.2 the model
itself has asave(), adelete() and arefresh_from_db(); related managers got aadd(), aset() and friends in the same
release. Calling the synchronous methods from a running event loop raises SynchronousOnlyOperation, so a sync save() in
an async def view fails loudly instead of blocking the loop. The ORM runs the query in a thread (sync_to_async with
thread-sensitive mode) behind the scenes, so async does not make queries faster; it keeps the event loop free.
Validation is the easy one to forget: full_clean() runs validate_unique() and validate_constraints(), which query,
so in async code wrap it with sync_to_async. Overrides of save() and delete() remain synchronous methods; asave()
calls them through the same machinery, so keep them free of blocking network I/O. File storage (FieldFile.save(),
delete()) is synchronous too. The tests below cover all of this:
class AsyncTests(TestCase):
async def test_async_methods(self):
author = await Author.objects.acreate(name="n", slug="n")
book = Book(title="Async", isbn=ISBN, price=1, author=author)
await book.asave()
book.pages = 7
await book.asave(update_fields=["pages"])
await book.arefresh_from_db()
self.assertEqual(book.pages, 7)
loaded = await Book.objects.select_related("author").aget(pk=book.pk)
self.assertEqual(loaded.author.name, "n")
await book.adelete()
self.assertEqual(await Book.objects.acount(), 0)
async def test_synchronous_orm_calls_are_refused_in_async_code(self):
with self.assertRaises(SynchronousOnlyOperation):
Author.objects.count()
self.assertEqual(await Author.objects.acount(), 0)
async def test_validation_that_queries_must_run_in_a_thread(self):
author = await Author.objects.acreate(name="n", slug="n")
book = Book(title="t", slug="t", isbn=ISBN, price=1, author=author)
with self.assertRaises(SynchronousOnlyOperation):
book.full_clean() # validate_unique() and validate_constraints() hit the database
await sync_to_async(book.full_clean)()
Source: Django: Asynchronous support and Model.asave()
Custom model fields
Write a custom field when a value has a richer Python representation than any built-in field provides, or when you need a
database column type Django lacks. Start from the closest existing field: subclassing TextField or CharField saves
most of the work, and subclassing Field is for everything else. The Bookshelf TagListField stores a list of tags in
one TEXT column as "sf,classic"; the Python attribute is a list:
from django import forms
from django.db import models
class TagListFormField(forms.CharField):
"""Shows a list of tags as 'a, b, c' in a text input."""
def prepare_value(self, value):
if isinstance(value, (list, tuple)):
return ", ".join(value)
return value
def to_python(self, value):
value = super().to_python(value)
return [tag.strip() for tag in value.split(",") if tag.strip()]
class TagListField(models.TextField):
"""A list of short tags stored in one TEXT column, joined by a separator."""
description = "A list of tags stored as one separated string"
def __init__(self, *args, separator=",", **kwargs):
self.separator = separator
kwargs.setdefault("default", list)
kwargs.setdefault("blank", True)
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.separator != ",":
kwargs["separator"] = self.separator
return name, path, args, kwargs
def from_db_value(self, value, expression, connection):
return self.to_python(value)
def to_python(self, value):
if value is None:
return []
if isinstance(value, (list, tuple)):
return list(value)
return [tag.strip() for tag in value.split(self.separator) if tag.strip()]
def get_prep_value(self, value):
if value is None or isinstance(value, str):
return value
return self.separator.join(tag.strip() for tag in value if tag.strip())
def value_to_string(self, obj):
return self.get_prep_value(self.value_from_object(obj))
def formfield(self, **kwargs):
kwargs.setdefault("form_class", TagListFormField)
return super().formfield(**kwargs)
What each part is for, in order of importance:
-
__init__splits your own arguments (separator) from the standard ones and passes the latter on.kwargs.setdefaultsupplies a callable default (list) andblank=True. -
deconstruct()tells migrations how to recreate the field. Every argument you add in__init__must come back out ofdeconstruct()(omitted when it equals the default, to keep migrations short), and arguments should be returned as keywords. The migration the project generated shows the result:catalog.fields.TagListField(blank=True, default=list). Because migrations import the class by path, never rename or move a field class that has been migrated; keep it for as long as any migration references it, and create a new class instead of changing the base class of an existing one. -
from_db_value()converts the raw column value into the Python object whenever a row is loaded, including invalues()and aggregates.to_python()does the same for deserialization and forclean(), and must accept an instance of the right type, a string andNone, raisingValidationErroron bad input. If you overridefrom_db_value()you must overrideget_prep_value()too, which converts the Python object into a query parameter; lookups such astags__contains="classic"call it with a plain string, so it must pass strings through. (On MySQL aCHAR/TEXTfield must always return a string fromget_prep_value().) -
value_to_string()is what the serializers (dumpdata,serializers.serialize) write;formfield()returns the form field, here aCharFieldsubclass whoseprepare_value()joins the list for display and whoseto_python()splits the submitted text. -
Not used here but common:
db_type(connection)returns the column type (returnNoneto create the column yourself) andrel_db_type()the type of foreign keys that point at the field;non_db_attrslists attributes that never change the column, so migrations skip a no-opAlterField;descriptionappears indjango.contrib.admindocs; and since 3.0descriptor_classlets a field customise attribute access.
class SeparatedField(models.TextField):
def __init__(self, *args, separator=",", **kwargs):
self.separator = separator
super().__init__(*args, **kwargs)
@property
def non_db_attrs(self):
return super().non_db_attrs + ("separator",)
class Money(models.Field):
def db_type(self, connection):
return "numeric(12, 2)" if connection.vendor == "postgresql" else "decimal"
The tests prove the round trip (including normalisation), the stored text, the lookups, deconstruct(), serialization
and the form field:
class CustomFieldTests(TestCase):
def test_round_trip_and_storage(self):
book = make_book(tags=["sf", " classic ", ""])
self.assertEqual(book.tags, ["sf", " classic ", ""]) # unchanged until reloaded
book.refresh_from_db()
self.assertEqual(book.tags, ["sf", "classic"])
with connection.cursor() as cursor:
cursor.execute("SELECT tags FROM catalog_book")
self.assertEqual(cursor.fetchone()[0], "sf,classic")
self.assertTrue(Book.objects.filter(tags__contains="classic").exists())
self.assertTrue(Book.objects.filter(tags="sf,classic").exists())
def test_deconstruct(self):
field = Book._meta.get_field("tags")
_name, path, _args, kwargs = field.deconstruct()
self.assertEqual(path, "catalog.fields.TagListField")
self.assertNotIn("separator", kwargs)
self.assertEqual(TagListField(separator=";").deconstruct()[3]["separator"], ";")
clone = TagListField(**TagListField(separator=";").deconstruct()[3])
self.assertEqual(clone.separator, ";")
def test_serialization_uses_value_to_string(self):
data = serializers.serialize("json", [make_book(tags=["sf", "classic"])])
self.assertIn('"tags": "sf,classic"', data)
self.assertEqual(next(serializers.deserialize("json", data)).object.tags, ["sf", "classic"])
def test_form_field_round_trip(self):
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ["title", "isbn", "price", "author", "tags", "status"]
form = BookForm({"title": "T", "isbn": ISBN, "price": "3", "author": make_author().pk,
"tags": "a, b ,c", "status": "draft"})
self.assertTrue(form.is_valid(), form.errors)
self.assertEqual(form.cleaned_data["tags"], ["a", "b", "c"])
self.assertEqual(form.fields["tags"].prepare_value(["a", "b"]), "a, b")
Prefer a built-in field when one fits. On PostgreSQL ArrayField already stores a list and supports lookups in SQL, and a
JSONField holding a list works everywhere; a custom field is justified when the column format is fixed by another system
or when you want the Python object to have behaviour.
Pitfalls and how to avoid them
| Mistake | What to do instead |
|---|---|
Assuming |
Call |
Expecting overridden |
They are bypassed ( |
|
Use |
|
Pass a callable that returns a fresh object ( |
|
Use |
|
|
Using |
|
Storing files or images in |
|
Changing |
|
Renaming a field and letting |
Answer the rename prompt, or write |
A |
It runs once per row in the admin and in templates; use |
Copying the |
Let the 6.0+ default ( |
Trusting that |
It narrows the window only; handle |
A sync ORM call inside |
|
|
It is not enforced; use |
Where the books are outdated
The four reference books were written for Django 1.11 to 3.x. For models these statements need updating:
| Older advice | Changed in | Current approach |
|---|---|---|
|
2.0 |
|
|
removed in 2.1 |
|
|
removed in 3.0 |
A plain |
Loose tuples of choices and string comparisons |
3.0 ( |
A nested enumeration passed as |
|
|
|
|
removed in 4.0 (a |
|
No |
setting added in 3.2; |
|
|
deprecated 4.2, removed 5.1 |
|
|
renamed in 5.1, |
|
Python-only |
|
Let the database supply defaults and derived columns. |
|
deprecated 5.1, removed 6.0 |
Keyword arguments only. |
Everything synchronous |
async ORM 4.1, |
|
Source: Django: Deprecation timeline
What changed recently
| Release | Change |
|---|---|
6.1 |
|
6.0 |
|
5.2 (LTS) |
|
5.1 |
|
5.0 |
|
4.2 |
|
4.1 |
|
3.2 |
|
3.1 |
|
3.0 |
|
Source: Django 6.1 release notes and Django 6.0 release notes
Testing models
Test the behaviour you wrote (validators, clean(), overrides, custom fields), not Django. Every example on this page is a
test in catalog/tests.py or catalog/test_snippets.py, run with python manage.py test catalog; the pattern is
TestCase (each test runs in a rolled-back transaction) and TransactionTestCase when the test creates tables or needs real
commits. Tools used above: isolate_apps for throw-away model classes, CaptureQueriesContext to assert query counts,
captureOnCommitCallbacks(execute=True) for on_commit hooks, and connection.schema_editor() to create a table for an
isolated model. For readable fixtures use factories rather than hand-written rows (see
Fixtures, Serialization & Initial Data); testing in general is
in Testing.
Source: Django: Testing tools