Models and migrations — the basics
How a Python class becomes a database table, and why Django makes you generate a migration file instead of just changing the schema directly.
3 min read
A model is a Python class that describes a database table
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published_at = models.DateTimeField(auto_now_add=True)
is_featured = models.BooleanField(default=False)Each class attribute becomes a column in the corresponding database table — title becomes a text column limited to 200 characters, body becomes an unbounded text column, and so on. Every model automatically inherits from models.Model, which is what gives it the database-talking behavior (saving, querying, deleting) covered in the next lessons. Django also adds an automatic id column (an auto-incrementing primary key) to every model unless you explicitly define your own.
The common field types, and what each one actually stores
models.CharField(max_length=200) # short text — max_length is required
models.TextField() # long text, no length limit
models.IntegerField() # whole numbers
models.FloatField() # decimal numbers
models.BooleanField(default=False) # True/False
models.DateTimeField(auto_now_add=True) # a timestamp, set automatically on creation
models.ForeignKey("Author", on_delete=models.CASCADE) # a link to another modelCharField requiring max_length (while TextField doesn't) reflects a real database-level distinction, not an arbitrary Django rule — many databases store short bounded text differently from unbounded text internally. ForeignKey is how one model relates to another (an Article belonging to an Author) — on_delete=models.CASCADE specifically means "if the related Author is deleted, delete this Article too," one of several defined behaviors for what should happen to dependent rows.
Migrations: how a model change actually reaches the database
Editing a model class doesn't touch the database at all by itself — Python code and the database schema are two separate things that have to be kept in sync deliberately:
python manage.py makemigrations # look at model changes, generate a migration file
python manage.py migrate # actually apply that migration to the databasemakemigrations compares your current models against the last-known state and writes a new file in migrations/ describing exactly what changed (add this column, remove that one, alter this field's type). migrate then runs that file's instructions against the actual database. Splitting it into these two steps — generate, then apply — means the exact schema change is captured as a reviewable, version-controlled file before it ever touches real data, rather than the database schema silently drifting to match whatever the models currently say.
Why a generated migration file, instead of just syncing automatically
A migration file is committed to git like any other code change — it can be code-reviewed, it runs identically across every developer's machine and every environment (dev, staging, production), and it can be rolled back with migrate app_name previous_migration_name if something's wrong. If Django just auto-synced the database to match the models on every change, there'd be no record of how the schema evolved over time, no way to review a schema change before it hits production, and no clean way to undo one specific change.
Using a model: creating, reading, updating, deleting
Article.objects.create(title="Hello", body="First post") # create
Article.objects.all() # read — every article
Article.objects.get(id=1) # read — exactly one, by id
Article.objects.filter(is_featured=True) # read — matching some condition
article = Article.objects.get(id=1)
article.title = "Updated title"
article.save() # update
article.delete() # deleteThis is CRUD (Create, Read, Update, Delete) — the four basic operations almost any data-backed application needs, and Article.objects (Django calls this the manager) is the entry point for all of them. .filter() in particular is the one worth internalizing early: it doesn't fetch anything from the database immediately — it builds up a description of a query, only actually run once you iterate over it or otherwise force it to produce results, a behavior covered in detail in the "QuerySets are lazy" lesson.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does each class attribute on a Django model class become?
2. Why does CharField require max_length while TextField doesn't?
3. What actually happens when you edit a Django model class, before running any commands?
4. What is Article.objects used for?