ποΈ Django Model Definition
A Django model is a single Python class that becomes a database table, a validation layer, an admin screen, and a query API all at once. Master model definitions and you have mastered the foundation of every Django app you will ever build.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Define a Django 5 model class and map its fields to database columns
- Choose the right field type and options (
null,blank,choices,default, validators) for each piece of data - Model the three relationships β one-to-many, many-to-many, and one-to-one β and pick the correct
on_deletebehavior - Use abstract base classes,
Metaoptions, model methods, and constraints to keep models clean and correct - Apply model best practices for readable, maintainable data layers
Estimated Time: 45β60 minutes β’ Difficulty: Intermediate
Hands-on: Design and write the full set of models for a small blog application.
In This Lesson
What a Model Really Is
In Django, a model is a Python class that subclasses django.db.models.Model. Each model maps to one database table, and each class attribute you declare as a Field maps to one column. You describe your data in Python; Django generates the SQL, builds an object-oriented query API, wires up the admin, and produces forms β all from that single definition.
π‘ A useful analogy: A model is like an architect's blueprint. The blueprint isn't the building β it's the authoritative description that everyone works from. Change the blueprint and the plumbing, wiring, and inspectors all follow. Change a Django model and its table, admin, forms, and validation all follow too.
This is the payoff of the DRY (Don't Repeat Yourself) principle: you define the shape of your data once, and Django derives everything else from it.
Defining Your First Model
Models live in an app's models.py. Here is a small, complete, modern example. Note the TextChoices enum (the current, readable way to declare choices in Django) and the __str__ method that gives each row a human-readable label.
from django.db import models
from django.conf import settings
from django.utils import timezone
class Article(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
ARCHIVED = "archived", "Archived"
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
content = models.TextField()
status = models.CharField(
max_length=10,
choices=Status.choices,
default=Status.DRAFT,
)
published_at = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="articles",
)
is_featured = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
π Key Terms
Field: a class attribute (an instance of a Field subclass) that becomes one database column.
Primary key: the unique row identifier. If you don't declare one, Django adds an id field automatically (a BigAutoField by default in modern Django).
settings.AUTH_USER_MODEL: the recommended way to reference the user model, so your code keeps working even with a custom user model.
β οΈ auto_now vs auto_now_add
auto_now_add=True stamps the time once, when the row is first created (great for created_at). auto_now=True re-stamps on every save (great for updated_at). Both make the field non-editable in forms.
Field Types & Options
Each field type maps to a database column type and carries its own validation. Pick the most specific field that fits your data β a specific field gives you free validation and a better admin widget.
| Category | Common fields | Use for |
|---|---|---|
| Text | CharField, TextField, SlugField, EmailField, URLField | Short strings, long prose, URL slugs, validated emails/URLs |
| Numbers | IntegerField, PositiveIntegerField, DecimalField, FloatField | Counts, money (Decimal!), measurements |
| Date/time | DateField, TimeField, DateTimeField, DurationField | Timestamps, schedules, elapsed time |
| Boolean | BooleanField | True/false flags (use null=True for a nullable boolean) |
| Files | FileField, ImageField | Uploaded documents and images |
| Special | JSONField, UUIDField, GenericIPAddressField | Structured blobs, UUID keys, IP addresses |
β οΈ Money = DecimalField, never FloatField
Floating-point math introduces rounding errors (0.1 + 0.2 != 0.3). For prices and any currency, always use DecimalField(max_digits=10, decimal_places=2).
Field options
Options are keyword arguments that tune a field's behavior. The most important ones:
from django.core.validators import MinValueValidator, MaxValueValidator
class Product(models.Model):
name = models.CharField(max_length=120, db_index=True)
sku = models.CharField(max_length=32, unique=True)
description = models.TextField(blank=True) # blank OK in forms
price = models.DecimalField(max_digits=10, decimal_places=2)
discount = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True, # optional in DB and forms
)
rating = models.IntegerField(
default=0,
validators=[MinValueValidator(0), MaxValueValidator(5)],
help_text="A score from 0 to 5.",
)
π‘ null vs blank β a classic gotcha
null is about the database (can this column store NULL?). blank is about form validation (can this field be left empty?). They are independent. For text fields, prefer blank=True alone and let "empty" be an empty string "" rather than NULL β that way there is only one representation of "no value." Add null=True only for non-text fields that genuinely have no value.
Model Relationships
Real data is connected. Django models three kinds of connection with three field types.
One-to-many β ForeignKey
One author writes many books; each book has one author. The ForeignKey lives on the "many" side.
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name="books", # author.books.all()
)
def __str__(self):
return self.title
π on_delete β what happens to the children?
Required on every ForeignKey. It decides what happens to Book rows when their Author is deleted:
CASCADEβ delete the books tooPROTECTβ block the delete if any book references this authorSET_NULLβ setauthortoNULL(requiresnull=True)SET_DEFAULTβ set it to the field's defaultRESTRICTβ like PROTECT, but allows deletion when another cascading path also removes the row
Many-to-many β ManyToManyField
An article can have many tags, and a tag can label many articles. Django creates a hidden join table for you.
class Tag(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name="articles", blank=True)
Put the ManyToManyField on either model β Django exposes the relationship from both sides. Use blank=True (not null=True) to make a many-to-many optional; a join table with zero rows already means "no relations."
One-to-one β OneToOneField
A user has exactly one profile. This is effectively a ForeignKey with unique=True and is the standard way to extend a model without touching it.
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
bio = models.TextField(blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f"Profile for {self.user}"
β Choosing a relationship
Ask "how many of B can one A have, and vice versa?" Oneβmany = ForeignKey. Manyβmany = ManyToManyField. Exactly oneβone = OneToOneField.
Model Inheritance
Django offers three inheritance styles. By far the most common β and usually the one you want β is the abstract base class for sharing fields without creating an extra table.
Abstract base classes (share fields, no table)
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # no table is created for this model
class Article(TimeStampedModel):
title = models.CharField(max_length=200)
class Comment(TimeStampedModel):
body = models.TextField()
Both Article and Comment now have created_at/updated_at columns, defined in one place.
Multi-table inheritance (each model gets a table)
Here a child model links to its parent with an automatic one-to-one. Both tables exist. Useful, but adds a JOIN on every access β reach for it deliberately.
class Place(models.Model):
name = models.CharField(max_length=100)
address = models.CharField(max_length=200)
class Restaurant(Place): # gets its own table + link to Place
serves_pizza = models.BooleanField(default=False)
Proxy models (same table, new behavior)
A proxy changes Python behavior (default ordering, extra methods) without changing the schema.
class OrderedArticle(Article):
class Meta:
proxy = True
ordering = ["title"]
π‘ Default to abstract
When in doubt, use abstract base classes. They give you code reuse with zero extra queries. Reserve multi-table inheritance for genuine "is-a" specializations you must query independently.
Meta Options & Constraints
An inner class Meta holds table-level configuration β ordering, verbose names, indexes, and database constraints.
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField()
published_at = models.DateTimeField()
class Meta:
ordering = ["-published_at"] # newest first by default
verbose_name = "article"
verbose_name_plural = "articles"
indexes = [
models.Index(fields=["-published_at"]),
models.Index(fields=["slug"]),
]
constraints = [
models.UniqueConstraint(
fields=["slug"], name="unique_article_slug"
),
]
β
Prefer constraints and indexes over field kwargs
Modern Django favors the explicit Meta.constraints list (e.g. UniqueConstraint, CheckConstraint) over older per-field options like unique_together. Constraints are enforced by the database itself, so they hold even if a bug bypasses your Python validation.
A CheckConstraint enforces a business rule at the database level:
class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(price__gte=0),
name="price_non_negative",
),
]
Model Methods
Fat models, thin views: put data-related logic on the model where it belongs. This keeps business rules in one place and out of your views and templates.
from django.urls import reverse
from django.utils import timezone
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
published_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.title
def get_absolute_url(self):
"""Canonical URL for this object β used by templates and the admin."""
return reverse("blog:article_detail", kwargs={"slug": self.slug})
@property
def is_recent(self):
"""True if published within the last 7 days."""
return timezone.now() - self.published_at <= timezone.timedelta(days=7)
@property
def word_count(self):
return len(self.content.split())
π get_absolute_url()
A conventionally named method returning the object's canonical URL. Define it and the admin's "View on site" button, plus {{ article.get_absolute_url }} in templates, just work.
A note on managers
The objects attribute on every model is a manager β the entry point for queries (Article.objects.all()). You can add custom managers to encapsulate common queries. Managers and their QuerySets are the focus of the next lesson, so we will only preview them here:
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status="published")
class Article(models.Model):
# ... fields ...
objects = models.Manager() # the default manager
published = PublishedManager() # Article.published.all()
Hands-on Exercise
ποΈ Build the models for a blog
Objective: Design the data layer for a small blog, applying fields, relationships, an abstract base class, and Meta options.
Requirements:
- A
TimeStampedModelabstract base withcreated_atandupdated_at. - A
Categorywithnameand a uniqueslug. - A
Postwith a title, unique slug, body, astatuschoice (draft/published), a ForeignKey toCategory, a ForeignKey to the user model as author, and a ManyToManyField of tags. - A
Commentwith a ForeignKey toPost(related_name="comments"), an author name, and a body. - Give every model a
__str__, and orderPostnewest-first inMeta.
π‘ Hint
Inherit both Post and Comment from TimeStampedModel. Use models.TextChoices for the status. Reference the user with settings.AUTH_USER_MODEL. A Tag model needs only a name and slug.
β Sample solution
from django.conf import settings
from django.db import models
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = "categories"
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Post(TimeStampedModel):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
body = models.TextField()
status = models.CharField(
max_length=10, choices=Status.choices, default=Status.DRAFT
)
category = models.ForeignKey(
Category, on_delete=models.SET_NULL, null=True,
related_name="posts",
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
related_name="posts",
)
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
class Comment(TimeStampedModel):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name="comments"
)
author_name = models.CharField(max_length=100)
body = models.TextField()
def __str__(self):
return f"Comment by {self.author_name} on {self.post}"
π― Quick Quiz
Question 1: You have middle_name = models.CharField(max_length=50, ...) and want it to be optional. What is the recommended option?
Question 2: Which on_delete value prevents deleting an author while any of their books still reference them?
Question 3: You want two models to share created_at/updated_at fields with no extra table and no extra JOINs. Which approach fits best?
Best Practices
β Do
- Always define
__str__for a readable admin and shell. - Reference the user with
settings.AUTH_USER_MODEL, neverUserdirectly. - Use
DecimalFieldfor money andTextChoicesfor enumerations. - Add
related_nameto relationships so reverse lookups read clearly. - Enforce invariants with database
constraints, and index fields you filter or order by. - Push data logic into model methods and properties (fat models, thin views).
β οΈ Don't
- Don't set
null=Trueon text fields β useblank=Trueinstead. - Don't store prices in a
FloatField. - Don't reach for multi-table inheritance when an abstract base class will do.
- Don't forget
on_deleteβ it is required and its choice has real data consequences.
Summary & Quiz
π Key Takeaways
- A model is one class that becomes a table plus validation, a query API, admin, and forms.
- Pick the most specific field type; tune it with options like
blank,choices,default, and validators. - Relationships:
ForeignKey(one-to-many),ManyToManyField(many-to-many),OneToOneField(one-to-one) β andon_deleteis mandatory. - Favor abstract base classes for shared fields, and enforce rules with
Metaconstraints and indexes. - Put behavior on the model with methods and properties.
π Further Reading
- Django Docs β Models
- Django Docs β Model field reference
- Django Docs β Model Meta options
- Django Docs β Constraints reference
π What's Next?
Your models describe the schema you want. In the next lesson, Database Migrations, you'll learn how Django turns those model definitions into real database tables β and how to evolve the schema safely as your models change.
π Well done!
You can now design a Django data layer from scratch. Time to make it real in the database.