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.

The concrete problem: say Bookshelf's 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:

CommandWhat it does
flask db initRun 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 upgradeApplies 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.

Note: Treat the generated migration as a first draft, not a finished product. A minute spent reading the 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.

Next up: Head to CRUD Operations to actually create, read, update, and delete Bookshelf's data through the session.