Use Flask Migrate for schema changes
Use Flask Migrate for schema changes — Python web development.
Focus: use flask migrate for schema changes
You’ve built a Flask app, your models look solid, and your database works — until you add a column to a model and nothing happens. Running db.create_all() again won’t update an existing table, and dropping the whole database to rebuild it is a quick trip to data-loss hell. This is the exact pain of schema changes without a migration system. In this lesson, you’ll use Flask-Migrate to evolve your database schema safely and incrementally, so your data stays intact and your team (or future self) can reproduce changes across every environment.
The problem this lesson solves
Imagine you shipped a User model with username and email. Now your product needs a bio field. If you just add bio = db.Column(db.String(200)) to the model and restart the app, Flask won’t know to alter the existing users table. You’ll get errors like no such column: user.bio in development or worse, a broken production database.
Manual ALTER TABLE statements work for one-off changes, but they’re unmanageable once you have:
- Multiple developers each changing the schema on their local database
- Staging and production environments that must match
- History — you need to know what changed and when
- Rollbacks — if a migration breaks, you want to undo it
Flask-Migrate (built on Alembic) solves this by tracking schema changes in versioned migration files. Each migration describes how to upgrade and downgrade the database, and Alembic applies them in order. This gives you a repeatable, scriptable way to evolve your schema without losing data.
Pro tip: The pain is real, but the fix is elegant. Once you adopt migrations, you’ll never
DROP TABLEin development again.
Core concept / mental model
Think of your database schema as a versioned document. Each migration is a git commit for your database — it records a snapshot of changes along with a way to apply and revert them.
Flask-Migrate is a Flask extension that wraps Alembic, a lightweight database migration tool for SQLAlchemy. It reads your db.Model classes and compares them to the current database state to autogenerate migration scripts.
Here’s the mental model:
- Migration file: A Python script with
upgrade()anddowngrade()functions. Theupgrade()applies the change (e.g., adds a column);downgrade()reverses it (e.g., drops the column). - Migration directory (
migrations/): A folder created byflask db initthat stores all migration files, plus Alembic configuration. - Version table: Alembic tracks which migrations have been applied in a special
alembic_versiontable in your database.
Think of the flow like this:
- You change a model.
- You run
flask db migrate— Alembic generates a migration script with the diff. - You review and edit the script if needed.
- You run
flask db upgrade— Alembic applies the change to the current database and updates the version table. - Later, you run
flask db downgradeto undo it if needed.
This abstraction lets you move from "manual ALTER statements" to a reproducible, team-friendly workflow.
How it works step by step
Flask-Migrate follows a predictable sequence. Here’s the high-level workflow you’ll use every time you make a schema change:
- Install Flask-Migrate and initialize it with your app.
- Set the FLASK_APP environment variable to your app entry point.
- Initialize the migration repository (
flask db init) — creates themigrations/folder. - Make a model change — add, remove, or alter columns, tables, etc.
- Autogenerate a migration (
flask db migrate -m "add bio column") — writes a script that captures the diff. - Review the generated script — never trust autogeneration blindly; check for missing indexes or data transformations.
- Apply the migration (
flask db upgrade) — executes theupgrade()function and updates the version. - Repeat for every schema change going forward.
The beauty is that step 4 and 5 become a tight loop: change model → generate migration → upgrade. Every teammate uses the same scripts, so local, staging, and production stay in sync.
Hands-on walkthrough
Let’s get your hands dirty. We’ll set up Flask-Migrate in a minimal Flask app and walk through a complete schema change.
Step 1: Set up the project
First, install the required packages:
pip install Flask Flask-SQLAlchemy Flask-Migrate
Create a file app.py with a basic app and a User model:
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'<User {self.username}>'
Note: We instantiate Migrate(app, db) right after db.create_all() would normally go — but we won’t use create_all anymore. We’ll let Alembic handle table creation.
Step 2: Initialize the migration repository
In your terminal, run:
export FLASK_APP=app.py # on Windows: set FLASK_APP=app.py
flask db init
This creates a migrations/ folder with all Alembic wiring.
Step 3: Create the initial migration
Since your database doesn’t exist yet, let the first migration create the User table:
flask db migrate -m "create users table"
flask db upgrade
You should see a migrations/versions/ file with a hash and the message. The upgrade() function in that file contains the CREATE TABLE statement.
Step 4: Add a column
Now, add a bio column to your model:
# app.py (inside User model)
bio = db.Column(db.String(200))
Generate a new migration and apply it:
flask db migrate -m "add bio column"
flask db upgrade
Inspect the generated migration file — you’ll see op.add_column('user', sa.Column('bio', sa.String(length=200), nullable=True)) and a downgrade() with op.drop_column. That’s your timeline of changes.
Step 5: Roll back (bonus)
To undo the last migration, run:
flask db downgrade
This executes the downgrade() function, dropping the bio column. Check your database — the column is gone, and your version table moved back one step.
Example: Complete migration file
Here’s what your second migration file should look like (auto-generated, but worth reading):
# migrations/versions/xxxxxxxxxxxx_add_bio_column.py
"""add bio column
Revision ID: xxxxxxxxxxxx
Revises: yyyyyyyyyyyy
Create Date: 2025-01-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = 'xxxxxxxxxxxx'
down_revision = 'yyyyyyyyyyyy' # points to previous migration
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('bio', sa.String(length=200), nullable=True))
def downgrade():
op.drop_column('user', 'bio')
Run flask db upgrade and flask db downgrade with this file to see the cycle in action.
Pro tip: Always review autogenerated migrations. Alembic can’t always detect data conversions (e.g., changing a string to an integer with existing data) — you may need to write custom logic in
upgrade().
Compare options / when to choose what
Flask-Migrate isn’t the only way to handle schema changes. Here’s how it stacks up against common alternatives:
| Approach | Use case | Pros | Cons |
|---|---|---|---|
| Flask-Migrate (Alembic) | Most Flask apps with SQLAlchemy | Versioned, team-friendly, autogenerated, rollback support | Slight learning curve, extra dependency |
| db.create_all() | Quick prototypes, scratch scripts | Single line, no setup | Only creates missing tables, can’t alter existing ones |
| Manual ALTER TABLE | One-off fixes, tiny databases | Full control, no dependency | Error-prone, no history, no rollback |
SQLAlchemy create_all() + drop_all() |
Demo apps where data loss is fine | Simple reset | Destroys data every time |
For real-world production apps, Flask-Migrate wins nearly every time because it gives you a repeatable, auditable path. Use db.create_all() only while prototyping on a throwaway database.
Variations and extensions
- Alembic directly: If you’re not using Flask, you can use Alembic standalone with any SQLAlchemy setup.
- Data migrations: Sometimes you need to transform existing data (e.g., split a
full_nameintofirst_nameandlast_name). You can write raw SQL or useop.bulk_insertinside a migration script. - Multi-database support: Flask-Migrate works with PostgreSQL, MySQL, SQLite, and more — just change the
SQLALCHEMY_DATABASE_URI.
Troubleshooting & edge cases
You will hit these issues — here’s how to fix them fast.
flask command not found
If flask db init throws Error: Could not import 'app', check whether your virtual environment is active and FLASK_APP is set correctly. Also, make sure app.py is in your current directory.
# Wrong
flask db init
# Correct
export FLASK_APP=app.py
flask db init
no such table: user on upgrade
This usually means you forgot to run the first migration. Run flask db upgrade from the start — Alembic will apply all pending migrations in order.
Autogenerated migration missed a column
Alembic doesn’t detect changes to column types or server defaults reliably. If you change a type from String to Integer, you must write the upgrade() manually:
def upgrade():
# Convert existing data — pseudocode
op.execute('ALTER TABLE user ALTER COLUMN age TYPE INTEGER USING age::integer')
flask db migrate says "No changes detected"
Your model and database are already in sync — or Alembic didn’t detect a subtle change. Double-check that you modified the model in the file that defines your db instance. Also ensure your model classes are imported so SQLAlchemy knows about them.
Downgrade fails
If a downgrade() relies on data that no longer exists, you’ll get an error. Always test your downgrades in development before relying on them in production.
What you learned & what's next
You’ve learned why use flask migrate for schema changes is essential for any serious Flask app. You now know how to install and initialize Flask-Migrate, generate migrations from model changes, apply them with flask db upgrade, and roll back with flask db downgrade. You’ve compared it to other approaches and can troubleshoot the most common pitfalls.
In this lesson, you covered both learning objectives: explaining the core idea behind Flask-Migrate and completing a practical exercise that adds a column to a table, then verifies it with a rollback.
What’s next? In the next lesson, you’ll learn how to handle database seeding and data migrations — moving from schema-only changes to populating your tables with initial data in a safe, repeatable way. You’ll build on the migration workflow you just mastered to keep your database in sync across every environment.
Now go add a column to your own project and see how confident you feel. Your future self (and your teammates) will thank you.
Practice recap
To reinforce this lesson, open your existing Flask project and add a new field to one of your models (e.g., phone_number on a Profile model). Run flask db migrate -m "add phone_number" and then flask db upgrade. Finally, run flask db history to see the list of migrations you've created, and then test flask db downgrade to undo the last change — you've now mastered schema evolution.
Common mistakes
- Forgetting to set FLASK_APP before running flask db commands — you'll get an 'Error: Could not import app' and wonder why nothing works.
- Running
flask db migratewithout making any model change — you'll see 'No changes detected' and waste minutes looking for a potential bug. - Trusting autogenerated migrations blindly, especially for column type changes or data transformations — always review the file and test it on a copy of your production data.
- Skipping
flask db upgradeafter generating a migration — your local database stays outdated, and you get confusing errors when your app reaches for the new column.
Variations
- Use Alembic directly (without Flask-Migrate) if you're building a non-Flask SQLAlchemy app — same concepts, but with manual configuration.
- Write data migrations inside your migration scripts using op.execute() or op.bulk_insert to transform existing rows during a schema change.
- Leverage Flask-Migrate with PostgreSQL and use named migrations for branching, so multiple developers can work on schema changes in parallel.
Real-world use cases
- E-commerce site adding a
discount_pricecolumn to a products table without taking the site offline during a seasonal sale. - SaaS platform rolling out a new user profile field (e.g., 'timezone') to millions of rows, using a migration to backfill default values safely.
- Multi-developer team working on the same Flask API — each member pulls the latest code, runs
flask db upgrade, and gets the exact same database schema as production.
Key takeaways
- Flask-Migrate (Alembic) turns your database schema into versioned, repeatable migration scripts — no more manual ALTER statements.
- The core loop is: change a model → run
flask db migrate→ review the script → runflask db upgrade. - Always define a
downgrade()in every migration so you can roll back safely. - Autogenerated migrations are a starting point, not the final word — review and edit for type changes and data transformations.
- Use
db.create_all()only for quick prototypes; for anything with real data, migrate with Flask-Migrate. flask db initsets up the migration repository once — after that, every environment can be synced withflask db upgrade.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.