Migrations, and how to not get stuck
You have models in Python, but the database knows nothing about them yet. Migrations are how Django turns a change in your models into a change in the database schema — and they are a version-controlled history of that schema, one of Django's genuinely excellent features. Used well, they make schema changes routine and safe; misunderstood, they are where beginners get most thoroughly stuck. This lesson makes them routine.
The two-command rhythm
Every schema change is two commands, and understanding the split between them is the whole game:
python manage.py makemigrations # 1. detect model changes, write a migration file
python manage.py migrate # 2. apply pending migrations to the database
makemigrationscompares your current models to the last recorded state, works out what changed, and writes a new migration file in the app'smigrations/directory. It does not touch the database — it only records intent.migratetakes the migration files that have not yet been applied and runs them against the database, executing the actualCREATE TABLE,ALTER TABLE, and so on.
So makemigrations describes the change and migrate performs it. When you first ran migrate in
the setup lesson with no migration files of your own, it applied Django's built-in apps' migrations (auth,
sessions). When you add a model, you run both.
What a migration actually is
A migration is a plain Python file describing operations. After defining Nidaan's first models,
makemigrations produced:
Migrations for 'patients':
patients/migrations/0001_initial.py
+ Create model Doctor
+ Create model Patient
+ Create model Appointment
Open 0001_initial.py and you will find a Migration class listing operations like
migrations.CreateModel(...). It is readable, and it is code you commit to git. That is the key
insight: your database schema's history lives in version control alongside your code, so every developer
and every server can build the exact same schema by running the same migrations in the same order. The
migration files are numbered (0001, 0002, …) and each records which migration it depends on, so Django
always knows the order.
Why this is better than editing the database by hand
Without migrations, a schema change means someone runs SQL against each database — dev, every teammate's
machine, staging, production — and hopes they all ran the same thing. They will not. Migrations replace that
with: change the model, makemigrations, commit the file, and everyone (and every server) runs migrate
to arrive at the identical schema. The migration files are the single source of truth for the schema,
and migrate is idempotent — running it when nothing is pending does nothing, so it is safe to run on every
deploy.
The commands that get you unstuck
Three read-only commands are your instruments when something is confusing — and per good practice, reach for them before changing anything:
python manage.py showmigrations # list every migration and whether it is applied ([X] or [ ])
python manage.py migrate --plan # show what `migrate` WOULD do, without doing it
python manage.py sqlmigrate patients 0001 # print the actual SQL a migration will run
showmigrations answers "what state is this database in?" — an unapplied migration shows [ ].
sqlmigrate prints the SQL a migration generates, which is both a great learning tool (see the
CREATE TABLE your model produced) and a way to review a risky change before it runs. Use these to
understand the situation rather than guessing.
The mistakes that get people stuck, and how to avoid them
Migrations trip beginners in a few predictable ways. Named plainly:
- Editing the database by hand, then wondering why migrations conflict. Once you use migrations, let
them own the schema. A manual
ALTER TABLEputs the database out of step with the migration history, and Django cannot reconcile it. - Adding a non-nullable field to a table with existing rows. If
Patientalready has rows and you addemail = models.CharField(max_length=200)with no default and nonull=True,makemigrationsasks: what value should the existing rows get? You must either provide adefault=, allownull=True, or supply a one-off value. This prompt is not a bug — the database genuinely cannot invent a value for existing rows, and Django is making you decide. - Forgetting to
makemigrationsafter changing a model. The model changed but no migration file was written, somigratehas nothing to apply and the database stays old. Symptom: "no such column". The fix is to runmakemigrations. - Not committing migration files. They are code. A teammate who pulls your model change but not your
migration file cannot reproduce your schema. Commit
migrations/*.pyalways.
None of these are mysterious once you hold the model: makemigrations writes files from model changes,
migrate applies files to the database, and the files are committed source of truth. Almost every "stuck"
situation is one of these four.
Rolling back, and starting over in development
You can migrate backwards by naming an earlier migration — python manage.py migrate patients 0001
reverses everything after 0001. This is useful in development to undo a change. In production, treat
reversals with great care (a reversal that drops a column destroys its data), which is why production
migrations are planned, not improvised. In early development, if your migrations get into a tangle you
cannot reason about, it is acceptable to delete the app's migration files and the dev db.sqlite3 and start
fresh — but only in development, never once real data exists. Knowing the difference between "throwaway
dev database" and "database with data you cannot recreate" is the line that matters.
Check your work
The two commands. makemigrations inspects model changes and writes a migration file (no DB change);
migrate applies pending files to the database (the actual schema change).
What a migration is. A committed, numbered Python file of operations (CreateModel, etc.) describing a
schema change — your schema's version-controlled history.
Why migrations beat hand-editing. Everyone and every server runs the same files in the same order to
reach the identical schema; migrate is idempotent and safe to run on every deploy.
The three read-only helpers. showmigrations (what is applied), migrate --plan (what would run),
sqlmigrate (the SQL a migration produces) — use them to understand before acting.
The four common ways to get stuck. Hand-editing the DB, adding a non-nullable field to populated tables
(provide a default/allow null), forgetting makemigrations ("no such column"), and not committing migration
files.
Reversing and resetting. You can migrate backwards to an earlier migration; deleting migrations + the dev DB to start fresh is fine only in development, never once real data exists.
Practice
- Add the
Patientmodel, runmakemigrations, and open the generated0001_initial.py— read theCreateModeloperation. - Run
sqlmigrate patients 0001and read the actualCREATE TABLESQL your model produced. - Run
showmigrationsbefore and aftermigrateand watch the[ ]become[X]. - Add a new non-nullable field to
Patient(with existing rows) and runmakemigrations; read the prompt Django gives you and choose a one-off default. Understand why it asked. - Change a model but do not run
makemigrations; try to use the new field and read the "no such column" error. Fix it withmakemigrations+migrate. - In development, migrate
patientsbackward to0001, then forward again withmigrate— a reversible change, observed.
Official documentation
- Django — Migrations — The complete guide.
- Django — Migration operations — Every operation a migration file can contain.
- Django — django-admin commands —
migrate,makemigrations,showmigrations,sqlmigrate.
Next: ForeignKey, ManyToMany and OneToOne.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship