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.
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.
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.
CharField- a short string;max_lengthis requiredTextField- a long text without a limitIntegerField,PositiveIntegerField,BigIntegerField- integers of various rangesDecimalField- an exact decimal number withmax_digitsanddecimal_places; use it for moneyFloatField- a floating point numberBooleanField- true or falseDateField,DateTimeField,TimeField- date and time valuesEmailField,URLField,SlugField- strings with validationUUIDField,JSONField,FileField,ImageField- special values
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.
null=True- the column may storeNULLin the databaseblank=True- the value may be empty in forms and in validation. It is independent ofnull; for string fields preferblank=Truealone and store the empty stringdefault- the default value or a callableunique=True- the values must be unique in the tablechoices- restricts the allowed valuesdb_index=True- creates a database indexverbose_nameandhelp_text- labels for forms and the admin siteauto_now_add=Trueandauto_now=True- on date fields, set the value at creation, or at every save
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.
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.
ForeignKey- a many-to-one relation: many books belong to one authorManyToManyField- many books have many tags; Django creates the join tableOneToOneField- one author has at most one profile
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.
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.
CASCADE- delete the dependent rows tooPROTECT- refuse the deletion by raisingProtectedErrorRESTRICT- likePROTECT, but allows the deletion when the object is deleted in the same operation through a cascadeSET_NULL- set the column toNULL; needsnull=TrueSET_DEFAULTandSET(value)- set a default or a given valueDO_NOTHING- do nothing; the database may report an integrity error
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.
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.
ordering- the default ordering of query resultsverbose_nameandverbose_name_plural- human-readable namesdb_table- the table name, instead of theapp_modeldefaultindexes- database indexes over several fieldsunique_togetherand the more flexibleconstraintsabstract = True- the model is only a base class and has no table
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.
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: 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
List all Django tutorials.