Migrations with Flask-Migrate
Bookshelf's models have been changing every lesson so far — and up to this point,
db.create_all() quietly handled it because there was no real data at stake yet. That
stops being true the moment Bookshelf has actual users with actual books logged. This lesson
covers Flask-Migrate, the tool that lets a schema change without anyone's
library disappearing.
Why db.create_all() Stops Being Enough
Recall from Flask-SQLAlchemy Basics that
db.create_all() only ever does one thing: it looks at every model currently imported
and creates a table for any that don't already have one. That's exactly what you want for a
brand-new project with an empty database. It is not what you want once
bookshelf.db already has rows in it, because create_all() has no idea
how to alter a table that already exists.
Book model needs a new
rating column added after real users already have books saved. Running
db.create_all() again does nothing at all for the existing book table
— it sees the table is already there and moves on, so the new column never gets added. The
only way to get that column in by hand would be writing raw ALTER TABLE SQL yourself,
and remembering to run it identically on every environment Bookshelf runs in. That's precisely
the manual, error-prone process Flask-Migrate exists to replace.
Flask-Migrate: Flask's Standard Migration Tool
Flask-Migrate is the standard extension for managing schema changes in a
Flask-SQLAlchemy project. It doesn't reinvent migrations itself — it's a Flask-friendly
wrapper around Alembic, SQLAlchemy's own migration engine, giving you a small set
of flask db ... commands instead of driving Alembic directly.
Install it the same way as Flask-SQLAlchemy, into Bookshelf's active virtual environment:
pip install flask-migrate
Initializing Migrate in the App Factory
Flask-Migrate follows the exact same module-level-then-init_app shape as
Flask-SQLAlchemy itself, and it needs a reference to db to know which models it's
tracking:
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# app.py
from flask import Flask
from extensions import db, migrate
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///bookshelf.db'
db.init_app(app)
migrate.init_app(app, db)
return app
Note that migrate.init_app(app, db) takes both the app and the shared
db object — Flask-Migrate needs to see your models through db in
order to compare them against the database's current structure.
The Migration Workflow: Three Commands
Day-to-day, Flask-Migrate comes down to three commands, run from Bookshelf's project root with the virtual environment active:
| Command | What it does |
|---|---|
flask db init | Run once per project. Creates a migrations/ folder that will hold every migration script Bookshelf ever generates. |
flask db migrate -m "message" | Compares your current models against the database's last known structure and auto-generates a migration script describing the difference — it does not touch the database yet. |
flask db upgrade | Applies any pending migration scripts to the actual database, bringing its structure in line with your models. |
Worked Example: Adding rating to Book
Bookshelf's Book model already has a rating column from
Models & Relationships — here's how that
column would have been added safely to a database that already had books saved, without losing a
single existing row.
Step 1 — set up migrations, once, early in the project:
flask db init
Step 2 — change the model (add the new field to the Book
class in models.py):
rating = db.Column(db.Integer)
Step 3 — generate a migration describing that change:
flask db migrate -m "add rating to book"
Flask-Migrate compares the updated Book model against the database's current
structure, notices the new rating column, and writes a migration script into
migrations/versions/ containing the SQL needed to add exactly that column —
existing rows are untouched, and simply get NULL for rating until each
book is rated.
Step 4 — actually apply it:
flask db upgrade
Only now does the book table in bookshelf.db actually gain the new
column. Every existing title, author, and page count saved before this migration stays exactly
as it was.
Reviewing a Migration Before Applying It
Between flask db migrate and flask db upgrade is a genuinely important
habit: open the generated file inside migrations/versions/ and actually read it
before running upgrade. Alembic's autodetection handles common cases — new
columns, new tables, new indexes — well, but it is not infallible. It can miss a column
rename entirely (seeing it as one column dropped and an unrelated one added, silently losing
every existing value in the process), and it never guesses at data transformations you might
actually want alongside a structural change.
upgrade() and downgrade() functions it produced is
far cheaper than discovering after the fact that a "safe" auto-generated migration dropped a
column full of real data.
Rolling Back with downgrade
flask db downgrade reverses the most recently applied migration, running its
downgrade() function to undo the change:
flask db downgrade
It's a genuinely useful escape hatch while developing locally. Used against a production database
it deserves real caution: a downgrade that removes a column doesn't archive that column's data
anywhere — it drops it, permanently, exactly as a manual ALTER TABLE ... DROP COLUMN
would. "Rolling back" sounds reversible; against real data it frequently isn't.