Alembic Autogenerate: Auto-Detect Schema Changes
Alembic's autogenerate feature compares your SQLAlchemy ORM models to your database schema and writes migration code automatically. When you add a column to a model, run alembic revision --autogenerate, and Alembic generates the SQL to add that column. This saves you from writing boilerplate SQL by hand and is the fastest path for teams using SQLAlchemy.
How Autogenerate Works
Alembic inspects two things: (1) your SQLAlchemy model definitions and (2) the current database schema. It computes the diff—what exists in your models but not the database, what's in the database but not your models—and generates Python code in the migration file that bridges the gap. The generated code uses Alembic's op object (a builder API) to express schema changes in a database-agnostic way.
For example, if you add email = Column(String(100)) to your User model, autogenerate produces:
op.add_column('users', sa.Column('email', sa.String(100), nullable=True))
This works on PostgreSQL, MySQL, SQLite, and other databases without modification.
Step 1: Define Your SQLAlchemy Models
Start with a clean models file. Use the SQLAlchemy 2.0+ declarative syntax:
# app/models.py
from sqlalchemy import Column, Integer, String, DateTime, create_engine
from sqlalchemy.orm import DeclarativeBase
from datetime import datetime
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
email = Column(String(100), unique=True)
created_at = Column(DateTime, default=datetime.utcnow)
Alembic's autogenerate reads this definition and compares it to your database schema.
Step 2: Link Your Models in env.py
Tell Alembic where your models live by importing your Base in migrations/env.py:
# migrations/env.py
from app.models import Base # Your declarative base
# ... (existing config code) ...
target_metadata = Base.metadata # Critical: without this, autogenerate can't see your models
The target_metadata is the source of truth for what your schema should look like. Without it, Alembic can't auto-detect changes.
Step 3: Create an Autogenerated Migration
Modify your model to add a column:
# app/models.py
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
email = Column(String(100), unique=True)
created_at = Column(DateTime, default=datetime.utcnow)
phone = Column(String(20)) # NEW COLUMN
Then run:
alembic revision --autogenerate -m "Add phone column to users"
Alembic inspects your models, finds the new phone column, and creates a migration file like 002_add_phone_column_to_users.py:
# migrations/versions/002_add_phone_column_to_users.py
def upgrade():
op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))
def downgrade():
op.drop_column('users', 'phone')
Open the file and review it. The generated code is usually correct, but autogenerate has limits (see below).
Step 4: Review and Apply
Always review generated migrations before applying them, especially in production. Check:
- Are the column types correct?
- Is the
nullableparameter what you expect? - Are indexes or unique constraints properly handled?
If the migration looks correct, apply it:
alembic upgrade head
What Autogenerate Can Detect
Alembic's autogenerate detects:
- New tables — creates
op.create_table()calls - Dropped tables — creates
op.drop_table()calls - New columns — creates
op.add_column()calls - Dropped columns — creates
op.drop_column()calls - Column type changes — creates
op.alter_column()calls - Nullable changes — updates
op.alter_column() - Unique constraints — creates
op.create_unique_constraint()calls - Indexes — creates
op.create_index()calls - Foreign keys — creates
op.create_foreign_key()calls
What Autogenerate CANNOT Detect
Autogenerate cannot automatically detect:
- Column renames — Alembic sees a drop and an add; you must edit the migration to use
op.alter_column(new_column_name=…) - Table renames — use
op.rename_table()manually - Data transformations — moving data between columns or computing new values requires custom Python in the migration
- Check constraints — use
op.create_check_constraint()in the migration manually - Custom collations — not always detected; add manually if needed
For these cases, you edit the generated migration by hand.
Example: Rename a Column Correctly
If you rename user_name to full_name in your model, autogenerate generates:
# Wrong autogenerate output (just drops and adds)
def upgrade():
op.drop_column('users', 'user_name')
op.add_column('users', sa.Column('full_name', sa.String(50)))
This loses data! Edit it to:
# Correct manual edit
def upgrade():
op.alter_column('users', 'user_name', new_column_name='full_name')
def downgrade():
op.alter_column('users', 'full_name', new_column_name='user_name')
Best Practices for Autogenerate
- Always review generated migrations. Autogenerate is a time-saver, not a replacement for thinking.
- Commit model changes and migrations together. When you add a column to a model, commit the model file and the migration file in the same commit.
- Run migrations on a test database first. Before applying to production, run
alembic upgrade headon a staging copy of your database and verify correctness. - Use version control for your migrations. Never modify an applied migration; create a new one.
- Disable autogenerate for complex changes. If you're doing custom data transformations, write the migration from scratch (see next article).
Autogenerate Configuration
You can configure autogenerate behavior in alembic.ini. For example, to ignore certain columns:
# alembic.ini
[alembic]
sqlalchemy.url = sqlite:///./app.db
include_schemas = true
render_as_batch = true # For SQLite, allows complex alter operations
Key Takeaways
- Autogenerate compares your SQLAlchemy models to your database and writes migration code automatically
- Use
alembic revision --autogenerate -m "message"after changing your models - Always review generated migrations for correctness before applying them
- Autogenerate handles most schema changes (new/dropped columns, tables, constraints) but not all (renames, data transforms)
- For complex changes, edit the generated migration or write it from scratch
Frequently Asked Questions
What if autogenerate produces an empty migration?
If you run alembic revision --autogenerate and the resulting file has an empty upgrade(), Alembic found no differences between your models and your database. This usually means your database already matches your models. You can delete the empty migration file (from the versions/ folder) and run again after making a real change.
Can I autogenerate migrations for an existing database?
Yes. Use alembic stamp <revision> to mark the current database state as an initial revision without creating a migration file. Then add columns to your models and autogenerate normally. This approach works if you have a legacy database and want to start tracking changes going forward.
How do I handle data migrations with autogenerate?
Autogenerate is schema-only; it cannot write the logic to migrate data (e.g., split name into first_name and last_name). Write these migrations by hand using the op.execute() function or Python loops. See the next article on manual migrations.
Why is my autogenerate producing SQL instead of Python?
Make sure target_metadata is set to Base.metadata in env.py. Without it, Alembic can't read your models and falls back to reading your database schema only (which is slower and less reliable).