ZetCode

Django models

last modified November 13, 2026 Python 3.15

In this article we define Django models: fields and their options, choices, the three kinds of relationships, the Meta class, constraints, and the on_delete behavior. The examples use Django 6.1, Python 3.15, and the default SQLite database.

Django models

A model is a Python class that describes a database table. It subclasses django.db.models.Model; each class attribute that is a field becomes a column. From the model Django generates the schema (see Django migrations), the SQL queries, and the default forms and administration pages. We work with objects and the ORM (object-relational mapper) instead of writing SQL by hand. Django adds an id auto-incrementing primary key unless we define our own.

Models live in the models.py module of an app, and the app must be listed in INSTALLED_APPS. Queries are the topic of Django QuerySets; here we create and relate objects.

The example project

The project is created with uv; the steps are explained in Django introduction. The commands below create a project directory, install Django, generate the project with the configuration package config, start the app library, and create the folder demos for demo scripts.

$ mkdir models
$ cd models
$ uv init --bare
$ uv add django
$ uv run django-admin startproject config .
$ uv run python manage.py startapp library
$ mkdir demos

The trailing dot in startproject config . creates manage.py and the config package in the current directory instead of one level deeper. A new app is ignored by Django until it is listed in INSTALLED_APPS, so we add the app to the list in config/settings.py; the other entries are generated by startproject.

config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'library',
]

The listings in this article are of two kinds. A listing with a solid blue border and the label Project file is a file of the application: models, views, templates, tests, and settings. A listing with a dashed amber border and the label Demo script is a small helper in the demos folder; it sets up Django, runs a few lines of code, and prints the results, so that we can watch the behaviour. Demo scripts are not part of the application; we run one from the project directory with uv run python -m demos.name.

We model a small library. There are authors, an optional profile for each author, publishers, tags, and books. Every model is described in the following sections.

library/models.py
from django.db import models
from django.db.models import Q


class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    born = models.DateField(null=True, blank=True)

    def __str__(self):
        return self.name


class AuthorProfile(models.Model):
    author = models.OneToOneField(Author, on_delete=models.CASCADE, related_name='profile')
    website = models.URLField(blank=True)
    bio = models.TextField(blank=True)


class Publisher(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)

    def __str__(self):
        return self.name


class Book(models.Model):

    class Status(models.TextChoices):
        DRAFT = 'D', 'Draft'
        PUBLISHED = 'P', 'Published'

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT, null=True, blank=True)
    tags = models.ManyToManyField(Tag, blank=True, related_name='books')
    pages = models.PositiveIntegerField(default=0)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    status = models.CharField(max_length=1, choices=Status, default=Status.DRAFT)
    published = models.DateField(null=True, blank=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['title']
        constraints = [
            models.CheckConstraint(condition=Q(price__gt=0), name='book_price_positive'),
        ]

    def __str__(self):
        return self.title

The next sections explain the parts of the file.

Fields and field types

Each field class maps a Python type to a database column type and to a form field. The most used fields appear in the models above.

The fields are also what the validation of forms is based on: an EmailField accepts only text that looks like an address in a form, but Django does not check it when we call save() directly; use full_clean() for that.

Field options

Every field accepts options that change its behavior. A few options are shared by all fields.

email = models.EmailField(unique=True)
born = models.DateField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

The author's e-mail must be unique, and the birth date is optional: it may be missing in the database (null) and in forms (blank). The timestamps are filled in by Django, so we never set them by hand.

Choices

A field with a limited set of values uses choices. The modern way is a nested class that extends models.TextChoices (or IntegerChoices); each member has a stored value and a readable label. The field then receives the class directly.

class Status(models.TextChoices):
    DRAFT = 'D', 'Draft'
    PUBLISHED = 'P', 'Published'

status = models.CharField(max_length=1, choices=Status, default=Status.DRAFT)

The database stores D or P. The get_status_display() method, which Django adds to every field with choices, returns the label. The values are validated in forms and by full_clean(), not by the database.

Creating the tables

Models are turned into tables with migrations. The makemigrations command writes the migration file, and the migrate command applies it. The sqlmigrate command prints the SQL that a migration runs; we show its output to see how the fields were mapped. The details are in Django migrations.

$ uv run python manage.py makemigrations library
Migrations for 'library':
  library/migrations/0001_initial.py
    + Create model Author
    + Create model Publisher
    + Create model Tag
    + Create model AuthorProfile
    + Create model Book
$ uv run python manage.py migrate library
Operations to perform:
  Apply all migrations: library
Running migrations:
  Applying library.0001_initial... OK

The generated SQL

The SQL for SQLite shows the table names (library_book: the app label and the model name), the column types, the foreign key columns with the _id suffix, the unique constraints, the check constraint from Meta, and the table behind the many-to-many field.

$ uv run python manage.py sqlmigrate library 0001
BEGIN;
--
-- Create model Author
--
CREATE TABLE "library_author" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL, "email" varchar(254) NOT NULL UNIQUE, "born" date NULL);
--
-- Create model Publisher
--
CREATE TABLE "library_publisher" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL);
--
-- Create model Tag
--
CREATE TABLE "library_tag" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL UNIQUE);
--
-- Create model AuthorProfile
--
CREATE TABLE "library_authorprofile" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "website" varchar(200) NOT NULL, "bio" text NOT NULL, "author_id" bigint NOT NULL UNIQUE REFERENCES "library_author" ("id") DEFERRABLE INITIALLY DEFERRED);
--
-- Create model Book
--
CREATE TABLE "library_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "slug" varchar(50) NOT NULL UNIQUE, "pages" integer unsigned NOT NULL CHECK ("pages" >= 0), "price" decimal NOT NULL, "status" varchar(1) NOT NULL, "published" date NULL, "created" datetime NOT NULL, "updated" datetime NOT NULL, "author_id" bigint NOT NULL REFERENCES "library_author" ("id") DEFERRABLE INITIALLY DEFERRED, "publisher_id" bigint NULL REFERENCES "library_publisher" ("id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "book_price_positive" CHECK ("price" > '0'));
CREATE TABLE "library_book_tags" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "book_id" bigint NOT NULL REFERENCES "library_book" ("id") DEFERRABLE INITIALLY DEFERRED, "tag_id" bigint NOT NULL REFERENCES "library_tag" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "library_book_author_id_d9a3b67e" ON "library_book" ("author_id");
CREATE INDEX "library_book_publisher_id_40a14e76" ON "library_book" ("publisher_id");
CREATE UNIQUE INDEX "library_book_tags_book_id_tag_id_c3660ce1_uniq" ON "library_book_tags" ("book_id", "tag_id");
CREATE INDEX "library_book_tags_book_id_7c6729bd" ON "library_book_tags" ("book_id");
CREATE INDEX "library_book_tags_tag_id_ad3c9ecb" ON "library_book_tags" ("tag_id");
COMMIT;

Creating objects

A model instance is a row. We create it with the constructor followed by save(), or with Model.objects.create(), which does both. The script below sets up Django as a standalone program with django.setup(), and creates an author, a publisher, and a book. The many-to-many relation is filled with add() after the book has been saved, because both rows need to exist first.

demos/create.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from decimal import Decimal

from library.models import Author, Book, Publisher, Tag

orwell = Author.objects.create(name='George Orwell', email='orwell@example.com')
penguin = Publisher.objects.create(name='Penguin')

book = Book(title='Nineteen Eighty-Four', slug='nineteen-eighty-four',
            author=orwell, publisher=penguin, pages=328,
            price=Decimal('9.99'), status=Book.Status.PUBLISHED)
book.save()

print(book, book.pk)
print(book.status, book.get_status_display())
print(Author._meta.db_table, Book._meta.db_table)
print(Book.Status.choices)

dystopia = Tag.objects.create(name='dystopia')
classic = Tag.objects.create(name='classic')
book.tags.add(dystopia, classic)
print(book.tags.count())

The script prints the object through __str__(), the stored status and its label, the table names, and the choices as a list of pairs. The Decimal value is passed as a string, to avoid the rounding errors of float numbers.

$ uv run python -m demos.create
Nineteen Eighty-Four 1
P Published
library_author library_book
[('D', 'Draft'), ('P', 'Published')]
2

The __str__ method

The __str__() method returns the readable name of an object. It is used by the shell, the admin site, and the default form widgets, so every model should define it. Without it, an object is shown as Book object (1).

def __str__(self):
    return self.title

The first line printed by the previous script, Nineteen Eighty-Four 1, is the result of this method followed by the primary key.

Relationships

Relational databases connect tables with keys. Django has three field types for that.

A relation field is declared on one side only. Django adds the reverse accessor to the other model; its name comes from the related_name option (without it, it is the lowercased model name with _set appended). The script below follows the relations in both directions and creates the profile.

author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
tags = models.ManyToManyField(Tag, blank=True, related_name='books')
author = models.OneToOneField(Author, on_delete=models.CASCADE, related_name='profile')

The first line is in Book and the last one in AuthorProfile. The ForeignKey stores the key in a column called author_id; book.author loads the author object on first access. The reverse accessors author.books and tag.books are managers, and the one-to-one reverse accessor author.profile returns the single object.

demos/relations.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from library.models import Author, AuthorProfile, Book, Tag

orwell = Author.objects.get(name='George Orwell')
book = Book.objects.get(slug='nineteen-eighty-four')

print(book.author.name, '/', book.publisher)
print(orwell.books.all())
print(sorted(tag.name for tag in book.tags.all()))
print(Tag.objects.get(name='classic').books.all())

AuthorProfile.objects.create(author=orwell, website='https://example.com/orwell')
print(orwell.profile.website)
print(orwell.profile.author is orwell)

The script reads the objects created before and follows the links: from the book to the author and the publisher, from the author to the books, from the book to the tags, and the other way around. The last two lines use the one-to-one relation in both directions.

$ uv run python -m demos.relations
George Orwell / Penguin
<QuerySet [<Book: Nineteen Eighty-Four>]>
['classic', 'dystopia']
<QuerySet [<Book: Nineteen Eighty-Four>]>
https://example.com/orwell
True

on_delete

A ForeignKey and a OneToOneField must say what happens with the row when the referenced row is deleted. The on_delete argument is required and takes one of the following values.

A book does not make sense without its author, so the author relation cascades. A publisher with books must not disappear silently, so that relation is protected. The script below tries to delete the publisher, tries to store a book with a price of zero, which violates the check constraint, and finally deletes the author. The delete() method returns the total number of deleted rows and a dictionary with the counts per model, so we see the cascade at work, including the rows of the join table and the profile.

demos/delete.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from decimal import Decimal

from django.db import IntegrityError
from django.db.models import ProtectedError

from library.models import Author, Book, Publisher

orwell = Author.objects.get(name='George Orwell')

try:
    Publisher.objects.get(name='Penguin').delete()
except ProtectedError as error:
    print('ProtectedError:', error.args[0])

try:
    Book.objects.create(title='Free', slug='free', author=orwell, price=Decimal('0'))
except IntegrityError as error:
    print('IntegrityError:', error)

print(orwell.delete())
print(Book.objects.count())
print(Publisher.objects.get(name='Penguin').delete())

The protected delete raises the error, and the zero price is rejected by the database. After the author is deleted, no books remain, and the publisher can be deleted.

$ uv run python -m demos.delete
ProtectedError: Cannot delete some instances of model 'Publisher' because they are referenced through protected foreign keys: 'Book.publisher'.
IntegrityError: CHECK constraint failed: book_price_positive
(5, {'library.Book_tags': 2, 'library.AuthorProfile': 1, 'library.Book': 1, 'library.Author': 1})
0
(1, {'library.Publisher': 1})

The Meta class

The inner Meta class sets options of the model as a whole. The common ones are the following.

class Meta:
    ordering = ['title']
    constraints = [
        models.CheckConstraint(condition=Q(price__gt=0), name='book_price_positive'),
    ]

The books are sorted by title when no other order is given. The CheckConstraint makes the database itself refuse a book with a non-positive price, which is why the earlier script failed with an IntegrityError; a constraint protects the data even from code that bypasses the model validation. The condition argument takes a Q object, a way to express a condition that we describe in Django QuerySets. A constraint needs a unique name.

Testing models

Model tests use TestCase, which wraps every test in a transaction that is rolled back at the end. The tests check the string representation and the default status, the reverse relation, the constraint, the protected publisher, and the cascading delete. A test that expects a database error runs the failing statement in transaction.atomic(), so the transaction of the test is still usable afterwards.

library/tests.py
from decimal import Decimal

from django.db import IntegrityError, transaction
from django.db.models import ProtectedError
from django.test import TestCase

from .models import Author, Book, Publisher


class BookModelTests(TestCase):

    def setUp(self):
        self.author = Author.objects.create(name='Jane Austen', email='jane@example.com')
        self.publisher = Publisher.objects.create(name='Penguin')
        self.book = Book.objects.create(
            title='Emma', slug='emma', author=self.author,
            publisher=self.publisher, price=Decimal('7.50'))

    def test_str_and_default_status(self):
        self.assertEqual(str(self.book), 'Emma')
        self.assertEqual(self.book.status, Book.Status.DRAFT)
        self.assertEqual(self.book.get_status_display(), 'Draft')

    def test_reverse_relation(self):
        self.assertEqual(list(self.author.books.all()), [self.book])

    def test_price_must_be_positive(self):
        with self.assertRaises(IntegrityError), transaction.atomic():
            Book.objects.create(title='Free', slug='free', author=self.author, price=Decimal('0'))

    def test_protected_publisher(self):
        with self.assertRaises(ProtectedError):
            self.publisher.delete()

    def test_deleting_author_deletes_books(self):
        self.author.delete()
        self.assertEqual(Book.objects.count(), 0)

Each test starts with the same two rows created in setUp(). The assertRaises() context manager checks that the code raises the given exception.

$ uv run python manage.py test
Found 5 test(s).
System check identified no issues (0 silenced).
Creating test database for alias 'default'...
.....
----------------------------------------------------------------------
Ran 5 tests in 0.006s

OK
Destroying test database for alias 'default'...

Source

Django documentation: Models

Django documentation: Model field reference

Django documentation: Model Meta options

In this article we have defined models with fields, choices, and the three relation types, controlled deletions with on_delete, and added a database constraint with Meta. The next articles continue with Django migrations, which create the tables, and Django QuerySets, which retrieve the data.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.