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 django.db.models.Model, declare fields as class attributes, run makemigrations and migrate.

Defining a model

Choosing a field

The most specific type that fits (DecimalField for money, UUIDField for public ids, TextField for unbounded text).

Field types

Optional values

blank=True lets a form leave it empty; null=True lets the database store NULL. Strings use "", not NULL.

null versus blank

A closed set of values

models.TextChoices / models.IntegerChoices, passed as choices=; enforced by validation, not by save().

Choices: TextChoices and IntegerChoices

Semi-structured data

JSONField(default=dict); relational columns for anything you filter or join on constantly.

JSONField

Values computed by the database

db_default for a default, GeneratedField for a column derived from other columns.

db_default, GeneratedField and db_comment

Primary keys

Let Django add the BigAutoField (default since 6.0); add a UUIDField for public identifiers.

Primary keys

Table-level behaviour

class Meta: ordering, indexes, constraints, verbose_name, db_table, managed, permissions.

The Meta options

Row-level behaviour

Methods: __str__, get_absolute_url, clean, and carefully written save() / delete() overrides.

Model methods

Rules that must never be broken

Database constraints; clean() and validators are not run by save().

Validation: what runs and when, Pitfalls and how to avoid them

Async code

asave(), adelete(), arefresh_from_db() (4.2); sync ORM calls raise SynchronousOnlyOperation.

Sync and async

A column Django does not know

Subclass Field (or an existing field): deconstruct, from_db_value, to_python, get_prep_value.

Custom model fields

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.

flowchart TD subgraph startup["Process start (django.setup)"] A["INSTALLED_APPS imported,
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 models module (stage 2 of django.setup()), the ModelBase metaclass turns each class attribute that is a Field into an entry of Model._meta, and only then does AppConfig.ready() run. This is why model modules must not do I/O and why ready() is the place for signal wiring. The system check framework then validates every model (the fields.E and models.E checks; an example is shown below) before runserver or migrate proceeds.

  • 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 in save() and delete(). With ATOMIC_REQUESTS = True the whole view runs in one transaction.

  • Commands and tests. migrate, loaddata, shell and 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.

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

catalog/validators.py
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")
catalog/models.py - the Author model
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

catalog/models.py - the Book model (imports and the Author model above it)
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

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

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):

config/settings.py and config/urls.py (only the changed lines)
INSTALLED_APPS = [
    # ... the default Django apps ...
    "catalog",
]

urlpatterns = [
    path("admin/", admin.site.urls),
    path("catalog/", include("catalog.urls")),
]

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);

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

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

Table 1. Built-in model fields (Django 6.1)
Field Python type Notes

AutoField, BigAutoField, SmallAutoField

int

Auto-incrementing primary keys. BigAutoField is the default for the automatic id since 6.0 (AutoField before). On PostgreSQL they are identity columns (since 4.1).

BigIntegerField, IntegerField, SmallIntegerField

int

64-, 32- and 16-bit. Values from -2147483648 to 2147483647 are safe in every database for IntegerField.

PositiveBigIntegerField, PositiveIntegerField, PositiveSmallIntegerField

int

Non-negative (0 is accepted); the database gets a CHECK constraint where it has no unsigned type, visible in the SQL above.

FloatField

float

Binary floating point; do not use it for money.

DecimalField(max_digits, decimal_places)

decimal.Decimal

Exact decimals. Both arguments are required on MySQL; since 6.1 they are optional on PostgreSQL, SQLite and Oracle.

BooleanField

bool

Defaults to None when no default is given. NullBooleanField was removed in 4.0: use BooleanField(null=True).

CharField(max_length)

str

Short text. max_length is enforced by validation and the database. Optional on PostgreSQL since 4.2 and on SQLite since 5.2; still required on MySQL, MariaDB and Oracle.

TextField

str

Unbounded text. A max_length is only reflected in the form widget, it is not enforced.

EmailField, URLField, SlugField

str

CharField subclasses with a validator (254, 200 and 50 characters by default). SlugField implies db_index=True; allow_unicode=True accepts Unicode letters.

DateField, DateTimeField, TimeField

datetime.date, datetime.datetime, datetime.time

auto_now (every save()) and auto_now_add (first save) make the field non-editable and blank.

DurationField

datetime.timedelta

interval on PostgreSQL, a bigint of microseconds elsewhere.

UUIDField

uuid.UUID

uuid on PostgreSQL and MariaDB, char(32) elsewhere. Pass the callable uuid.uuid4 as default.

BinaryField

bytes

Not editable by default; storing files in the database is almost always the wrong design. Since 6.1 it validates Base64 input strictly.

JSONField(encoder, decoder)

dict, list, str, numbers, bool, None

Supported on every database Django supports (SQLite needs the JSON1 extension). Since 3.1; see JSONField.

FileField(upload_to, storage), ImageField

FieldFile

A varchar path plus a storage backend; ImageField needs Pillow and can fill width_field / height_field.

FilePathField(path, match, recursive)

str

Choices limited to files in a directory; path may be a callable.

GenericIPAddressField(protocol)

str

IPv4 or IPv6; blank values are stored as NULL, so blank=True requires null=True.

GeneratedField(expression, output_field, db_persist)

the output_field type

A column the database computes from other columns (5.0). See db_default, GeneratedField and db_comment.

CompositePrimaryKey(*field_names)

tuple

A virtual field that makes several columns the primary key (5.2). See Constraints, Indexes & Composite Keys.

ForeignKey, OneToOneField, ManyToManyField

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

Table 2. Options common to all fields
Option What it does

null

Store empty values as NULL. Database-level only. Default False. See null versus blank.

blank

Validation-level: allow an empty value in forms and full_clean(). Default False. See null versus blank.

choices

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.

db_column

The column name, when it must differ from the attribute (a reserved word, a legacy schema).

db_comment

A column comment (4.2), for people who read the schema directly. Ignored (with check fields.W163) where unsupported.

db_default

A database-computed default: a literal or an expression built from literals and database functions (5.0). See db_default, GeneratedField and db_comment.

db_index

Create an index on the column. Prefer Meta.indexes, which can do much more; the docs warn db_index may be deprecated in future. Redundant with unique=True.

db_tablespace

The tablespace of this field’s index; defaults to DEFAULT_INDEX_TABLESPACE, then the model’s db_tablespace. Ignored where tablespaces are not supported.

default

A value or a callable called for every new instance. Never a mutable object: use default=dict, not default={}, and never a lambda (migrations cannot serialize it). Takes precedence over db_default in Python.

editable

False hides the field from the admin and every ModelForm and skips it during validation.

error_messages

Overrides messages by key: null, blank, invalid, invalid_choice, unique, unique_for_date, plus field-specific keys. They often do not propagate to forms.

help_text

Text shown with the form widget. It is not HTML-escaped in generated forms.

primary_key

Make this the primary key (implies null=False and unique=True). Only one field per model; use CompositePrimaryKey for several columns.

unique

Enforced by the database and by validation. Not allowed on ManyToManyField or OneToOneField.

unique_for_date, unique_for_month, unique_for_year

Unique per date component of another date field. Enforced by validation only, not by the database.

verbose_name

Human-readable name; derived from the attribute (underscores to spaces) when omitted.

validators

A list of callables run by clean_fields(). See Validation: what runs and when.

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"

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"

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"

null versus blank

The two options look alike and do unrelated jobs, and mixing them up is the most common model mistake.

  • null is about storage: null=True lets the database column hold NULL. It changes the CREATE TABLE.

  • blank is about validation: blank=True lets full_clean() and every ModelForm accept an empty value. It never touches the database.

Table 3. The four combinations (a text field and a non-text field behave the same except where noted)
Setting "" / empty form value None When to use it

neither (the default)

rejected: "This field cannot be blank."

rejected by validation, IntegrityError from the database

A required value.

blank=True

accepted

rejected for a string field (use "")

Optional text (Author.bio, Author.website): "no data" is the empty string.

null=True

rejected

accepted by the database, but rejected by validation (blank=False); save() itself stores it

Rarely useful by itself: the database allows NULL but the admin, forms and full_clean() still demand a value.

null=True, blank=True

accepted

accepted

Optional non-text values (Book.pages, Author.born, Book.published_on) where NULL is the honest "unknown".

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"])

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

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"

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")

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;

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.

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"

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

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_default is 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, default wins in Python and db_default still protects the table. On an unsaved instance, a field with db_default and no value holds a DatabaseDefault placeholder (shown in the shell session above), and the real value is read back from the INSERT …​ RETURNING clause after save().

  • GeneratedField is 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) and db_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 no default and 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_comment and Meta.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"

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);

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)

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_FIELD was introduced in 3.2. From then until 5.2 the startproject and startapp templates wrote BigAutoField explicitly, but the fallback default stayed AutoField; 6.0 made BigAutoField the 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 pin DEFAULT_AUTO_FIELD = "django.db.models.AutoField" (or default_auto_field in 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 id fields of type AutoField; do not copy their AutoField foreign-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"

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"

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):

  1. clean_fields() converts each field with to_python() and runs its validators, choices, null and blank checks;

  2. clean() is your hook for rules that involve several fields (see Book.clean() above);

  3. validate_unique() checks unique, unique_for_date/month/year and unique_together with a query;

  4. validate_constraints() (4.1) evaluates Meta.constraints in 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 ValidationError for a bad value. Give the error a code so callers and tests can branch on it (invalid_isbn above), and keep validators importable at module level so migrations can serialize them. A lambda cannot be serialized.

  • In clean(), raise ValidationError({"field": "message"}) to attach an error to a field; a bare message goes under NON_FIELD_ERRORS. A dictionary that names a field excluded from a ModelForm raises ValueError; override clean_fields(exclude=None) instead when the form may omit the field.

  • unique=True is checked twice: by validation (a friendly message, a race is possible between the check and the insert) and by the database (the authoritative IntegrityError). Catch IntegrityError around the write when two requests can create the same value concurrently.

  • A CheckConstraint is also checked twice. The test shows validate_constraints() rejecting a negative price in Python and the database rejecting a bulk update() that bypasses Python. violation_error_code (5.0) and violation_error_message customise the error. The syntax of CheckConstraint(condition=…​) changed in 5.1 (the check argument was deprecated then and removed in 6.0).

  • full_clean() and validate_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.

Table 4. Every documented Meta option
Option Effect

abstract

The class is a base for other models and has no table. See Model Inheritance & the Meta API.

app_label

Required when a model is defined outside a package listed in INSTALLED_APPS.

base_manager_name, default_manager_name

Which manager supplies _base_manager and _default_manager; see Managers & Custom QuerySets.

db_table

The table name; use lower case, especially on MySQL. Derived as <app_label>_<model> by default.

db_table_comment

A table comment (4.2); models.W046 on databases that cannot store one.

db_tablespace

The tablespace for the table (default DEFAULT_TABLESPACE); ignored where unsupported.

default_related_name

The default reverse accessor name (otherwise <model>_set); also sets related_query_name. Include %(app_label)s and %(model_name)s in abstract bases.

get_latest_by

The field(s) used by latest() and earliest().

managed

False stops migrations from creating, altering or dropping the table (legacy tables, database views).

order_with_respect_to

Makes rows orderable within a parent (adds an _order column and get_RELATED_order() helpers); cannot be combined with ordering.

ordering

The default ORDER BY: field names with an optional -, ? for random, or expressions such as F("released").desc(nulls_last=True).

permissions, default_permissions

Extra permissions, and the list of automatic ones (add, change, delete, view); set before the first migrate.

proxy

A subclass that adds Python behaviour but shares the parent’s table.

required_db_features, required_db_vendor

Skip the model in migrations unless the connection has the feature or is that vendor (sqlite, postgresql, mysql, oracle).

select_on_save

Use the pre-1.6 SELECT-then-UPDATE save algorithm; only for databases whose UPDATE result is misreported (for example a PostgreSQL ON UPDATE trigger that returns NULL).

indexes

A list of models.Index, including functional, partial and covering indexes; see Constraints, Indexes & Composite Keys.

constraints

A list of CheckConstraint and UniqueConstraint objects, enforced by the database.

unique_together

Sets of columns that must be unique together. The docs recommend UniqueConstraint instead and say it may be deprecated in the future.

verbose_name, verbose_name_plural

Human-readable names (a munged CamelCase and "s" by default); the admin and generic views use them.

label, label_lower

Read-only: catalog.Book and catalog.book.

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"),
        ]

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"]

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

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

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>')
catalog/urls.py
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"),
]

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 a DatabaseDefault), Django issues an INSERT.

  • If it is set, Django issues an UPDATE, and falls back to an INSERT when no row matched.

  • If the primary key field has a default or db_default (a UUIDField with default=uuid.uuid4), a new instance is INSERT`ed directly and only an existing instance is `UPDATE`d, so the extra `UPDATE attempt 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

  • pk aliases 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__ is hash(pk) and raises TypeError for an unsaved instance, so an unsaved model cannot go in a set or be a dict key.

  • _state.adding is True until the instance is saved or loaded, _state.db is the alias it was loaded from and, since 6.1, _state.fetch_mode is the fetch mode used for later field loads. _is_pk_set() tells whether the key is set; a DatabaseDefault counts as unset since 6.1.

  • For a field with choices Django adds get_FOO_display(); for every non-null DateField or DateTimeField it adds get_next_by_FOO() and get_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 nullable published_on.

  • To duplicate a row, set pk = None and _state.adding = True (and reset unique values), then save().

  • Assigning a value does not convert it. book.price = "12.50" leaves a str in memory until you reload or validate; the conversion (and its errors) happens in full_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 RuntimeWarning is 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)()

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:

catalog/fields.py
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.setdefault supplies a callable default (list) and blank=True.

  • deconstruct() tells migrations how to recreate the field. Every argument you add in __init__ must come back out of deconstruct() (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 in values() and aggregates. to_python() does the same for deserialization and for clean(), and must accept an instance of the right type, a string and None, raising ValidationError on bad input. If you override from_db_value() you must override get_prep_value() too, which converts the Python object into a query parameter; lookups such as tags__contains="classic" call it with a plain string, so it must pass strings through. (On MySQL a CHAR/TEXT field must always return a string from get_prep_value().)

  • value_to_string() is what the serializers (dumpdata, serializers.serialize) write; formfield() returns the form field, here a CharField subclass whose prepare_value() joins the list for display and whose to_python() splits the submitted text.

  • Not used here but common: db_type(connection) returns the column type (return None to create the column yourself) and rel_db_type() the type of foreign keys that point at the field; non_db_attrs lists attributes that never change the column, so migrations skip a no-op AlterField; description appears in django.contrib.admindocs; and since 3.0 descriptor_class lets 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

Table 5. Frequent model mistakes
Mistake What to do instead

Assuming save() validates (a published book without a date reaches the table)

Call full_clean() before save() outside forms, and put the invariants that must hold into constraints.

Expecting overridden save() / delete() to run for bulk_create(), bulk_update(), QuerySet.update(), QuerySet.delete() or a cascade

They are bypassed (bulk_* and update() never call save(), pre_save or post_save). Use signals for deletes, and database defaults, generated columns and constraints for rules that must always apply.

null=True on CharField / TextField

Use "", except for unique=True, blank=True (then null=True).

default=[], default={} or a lambda

Pass a callable that returns a fresh object (default=list, default=dict) defined at module level.

auto_now_add=True when you must be able to set the value later (it ignores what you pass)

Use default=timezone.now and editable=False, or a db_default=Now().

default=timezone.now() (called at import time, one fixed moment for every row)

default=timezone.now, without parentheses.

Using FloatField for money or DecimalField built from a float

DecimalField with Decimal("9.99") or a string; never Decimal(9.99).

Storing files or images in BinaryField

FileField / ImageField with a storage backend; validate what users upload and never serve uploads from the code directory.

Changing choices and expecting a schema change (or a stale form after a data change)

choices are validation only: the migration is a no-op. Use a table and a foreign key when the set changes at runtime.

Renaming a field and letting makemigrations guess

Answer the rename prompt, or write RenameField; otherwise the column and its data are dropped and re-created. See Migrations.

A __str__ or property that follows relations

It runs once per row in the admin and in templates; use select_related() or avoid the lookup.

Copying the AutoField of old tutorials, or never setting DEFAULT_AUTO_FIELD on a project older than 3.2

Let the 6.0+ default (BigAutoField) apply, or pin the existing type explicitly.

Trusting that unique=True validation prevents duplicates

It narrows the window only; handle IntegrityError for concurrent writes.

A sync ORM call inside async def

await the a* method, or wrap the sync function in sync_to_async.

TextField(max_length=…​) as a limit

It is not enforced; use CharField or a validator.

Where the books are outdated

The four reference books were written for Django 1.11 to 3.x. For models these statements need updating:

Table 6. Outdated model advice and the current form
Older advice Changed in Current approach

ForeignKey(Author) with no on_delete

2.0

on_delete is required in models and migrations; choose it deliberately (PROTECT in Book.author).

@models.permalink on get_absolute_url()

removed in 2.1

reverse() inside the method.

six, @python_2_unicode_compatible, __unicode__

removed in 3.0

A plain __str__.

Loose tuples of choices and string comparisons

3.0 (TextChoices, IntegerChoices); 5.0 (enumeration class, mapping, callable accepted directly)

A nested enumeration passed as choices=Status.

django.contrib.postgres.fields.JSONField

models.JSONField in 3.1; the postgres class removed in 4.0

models.JSONField, on any database.

NullBooleanField

removed in 4.0 (a fields.E903 error now)

BooleanField(null=True).

No DEFAULT_AUTO_FIELD; every id an AutoField

setting added in 3.2; BigAutoField default in 6.0

BigAutoField, and default_auto_field in reusable apps.

Meta.index_together

deprecated 4.2, removed 5.1

Meta.indexes with models.Index(fields=[…​]).

CheckConstraint(check=…​)

renamed in 5.1, check removed in 6.0

CheckConstraint(condition=Q(…​)).

Python-only default for columns that other writers also fill

db_default and GeneratedField in 5.0

Let the database supply defaults and derived columns.

save() called with positional arguments

deprecated 5.1, removed 6.0

Keyword arguments only.

Everything synchronous

async ORM 4.1, asave() / adelete() 4.2

a* methods in async code.

What changed recently

Table 7. Model-related changes by release (verified against the release notes on 2026-10-03)
Release Change

6.1

DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT for on_delete; JSONNull() and the deprecation of exact-matching None to mean JSON null; virtual GeneratedField on PostgreSQL 18+ and stored ones on Oracle 23ai/26ai; DecimalField without precision on PostgreSQL, SQLite and Oracle; from_db() gains fetch_mode and _state.fetch_mode appears; the blank choice label is - Select an option -; UUID4 / UUID7 database functions; BinaryField validates Base64 strictly; QuerySet.totally_ordered; m2m_changed receives raw.

6.0

BigAutoField becomes the default and the template lines are removed; GeneratedField values are refreshed after save() (or deferred on MySQL and MariaDB); Model.NotUpdated for a forced update that matches nothing; positional arguments to save() are removed; constraints get a check() method; CheckConstraint(check=) removed.

5.2 (LTS)

CompositePrimaryKey; CharField.max_length optional on SQLite; constraints that use a GeneratedField can be validated; shell auto-imports of models.

5.1

Meta.index_together removed; refresh_from_db(from_queryset=); CheckConstraint(condition=); positional save() arguments deprecated.

5.0

db_default; GeneratedField; choices accept mappings, callables and enumeration classes; violation_error_code on constraints; force_insert takes a tuple of parent classes.

4.2

db_comment and Meta.db_table_comment; asave(), adelete(), arefresh_from_db(); CharField.max_length optional on PostgreSQL; index_together deprecated.

4.1

Model.validate_constraints(); Field.non_db_attrs; PostgreSQL identity columns for auto fields; XOR in Q.

3.2

DEFAULT_AUTO_FIELD and AppConfig.default_auto_field; functional indexes and covering indexes.

3.1

models.JSONField on all databases; PositiveBigIntegerField; RESTRICT for on_delete.

3.0

TextChoices / IntegerChoices; SmallAutoField; Field.descriptor_class.

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.