12.14. Alembic
Table of Contents
Why Alembic Matters
When you build a real application, your database schema will change many times. You will:
- Add new tables.
- Add or remove columns.
- Change column types.
- Add or drop indexes and constraints.
If you do this manually with raw ALTER TABLE statements, you quickly get:
- Out of sync environments, for example dev vs production.
- Confusing database state, for example, "Did we already add this column?"
- Hard, error prone deployments.
Alembic solves this by giving you versioned database migrations for SQLAlchemy based projects.
With Alembic you:
- Describe changes to the schema in Python migration files.
- Apply them step by step to any database.
- Roll them back if you need to.
Alembic is the standard migration tool for SQLAlchemy. If you use SQLAlchemy in production, you should use Alembic or some other migration tool, never manual SQL only.
This chapter focuses on how to use Alembic, not on designing schemas or writing SQL. Those topics are covered in other chapters.
Core Concepts
Alembic is a migration engine. Its main concepts are:
| Concept | Description |
|---|---|
| Migration | A small step that changes the schema, for example, add a column. |
| Revision | A migration file, identified by a unique revision ID. |
| Upgrade | Apply a migration, move schema to a newer version. |
| Downgrade | Reverse a migration, move schema to an older version. |
| Revision history | A chain of revisions, similar to git commits. |
| Head | The latest revision in your migration history. |
| Alembic environment | The configuration and script folder that defines how migrations run. |
Each Alembic revision file contains two functions:
def upgrade():
# code that applies the migration
def downgrade():
# code that reverses the migration
Alembic also maintains a special table in your database called something like alembic_version. It stores the current revision of the schema for that database.
Setting Up Alembic in a Project
Installing Alembic
Alembic is a Python package. Install it in your project environment:
pip install alembicYou should run all Alembic commands from your project root, inside your virtual environment.
Initializing Alembic
To create Alembic configuration and migrations directory:
alembic init alembicThis creates:
alembic.ini(top level file)alembic/folder, containing:env.pyscript.py.makoversions/(empty folder for migration scripts)
Typical project tree after initializing:
my_app/
app/
__init__.py
models.py
db.py
alembic/
env.py
script.py.mako
versions/
alembic.ini
requirements.txtConfiguring Alembic
Alembic needs to know:
- How to connect to your database.
- How to import your SQLAlchemy
Baseand models, for autogeneration.
Database URL in `alembic.ini`
Open alembic.ini. You will see something like:
[alembic]
script_location = alembic
sqlalchemy.url = driver://user:pass@localhost/dbnameThere are two main patterns.
Pattern 1: Put URL directly here
For simple projects or local testing:
sqlalchemy.url = postgresql+psycopg2://user:password@localhost:5432/mydbPattern 2: Read URL from environment or your code
More common in real applications. You leave sqlalchemy.url blank or dummy, and configure inside env.py.
Example alembic.ini:
sqlalchemy.url = driver_not_used://
Then in alembic/env.py you load environment variables or import from your app.
Configuring `env.py` to use your models
Open alembic/env.py. Find something like:
from alembic import context
from sqlalchemy import engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file
config = context.config
target_metadata = None
You must set target_metadata to the MetaData of your SQLAlchemy models.
Assume you have something like this in app/db.py:
from sqlalchemy.orm import declarative_base
Base = declarative_base()
And your models in app/models.py inherit from Base.
Then modify env.py:
import os
import sys
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# Add project root to sys.path so imports work
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.db import Base # import your Base
config = context.config
fileConfig(config.config_file_name)
# Here, tell Alembic about your models
target_metadata = Base.metadataNow Alembic can see your tables and generate migrations from them.
Reading the database URL from your app
If your app defines a DATABASE_URL or similar, you can reuse it.
Example app/db.py:
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL, future=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()
In alembic/env.py, instead of using the sqlalchemy.url from the ini, you can set it:
from app.db import DATABASE_URL
config.set_main_option("sqlalchemy.url", DATABASE_URL)
A minimal env.py section might look like:
from alembic import context
from sqlalchemy import engine_from_config, pool
from app.db import Base, DATABASE_URL
config = context.config
config.set_main_option("sqlalchemy.url", DATABASE_URL)
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()You do not need to fully understand offline vs online mode now. For most use cases you will run migrations in online mode with a live database.
Creating Migrations
There are two main ways to create migrations:
- Autogenerate from your SQLAlchemy models.
- Manual migrations that you write yourself.
You often combine both.
Making your first autogeneration
Assume:
- You have models defined and imported via
Base.metadatainenv.py. - The database is empty.
Run:
alembic revision --autogenerate -m "create initial tables"Alembic will:
- Compare your
Base.metadata(what models say should exist) with the actual database. - Generate a migration script in
alembic/versions/with a random looking revision ID in the filename.
Example filename:
alembic/versions/1975ea83b712_create_initial_tables.pyOpen the created file. You will see something like:
"""create initial tables"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic
revision = '1975ea83b712'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('email', sa.String(), nullable=False, unique=True),
sa.Column('hashed_password', sa.String(), nullable=False),
)
def downgrade():
op.drop_table('users')
op is Alembic’s helper object. You use it to declare schema changes.
Always open and review autogenerated migrations. Never trust autogeneration blindly. Fix issues by editing the migration file before running it.
Common operations with `op`
Here are typical examples of Alembic operations that you will use.
Create a new table
def upgrade():
op.create_table(
'posts',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('title', sa.String(200), nullable=False),
sa.Column('content', sa.Text, nullable=False),
sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id'), nullable=False),
)
def downgrade():
op.drop_table('posts')Add a new column
def upgrade():
op.add_column('users', sa.Column('full_name', sa.String(200), nullable=True))
def downgrade():
op.drop_column('users', 'full_name')Rename a column
SQLAlchemy does not have a direct cross database rename. Alembic has op.alter_column with new_column_name, but not all databases support it. Example for PostgreSQL:
def upgrade():
op.alter_column('users', 'full_name', new_column_name='name')
def downgrade():
op.alter_column('users', 'name', new_column_name='full_name')Add an index
def upgrade():
op.create_index('ix_users_email', 'users', ['email'], unique=True)
def downgrade():
op.drop_index('ix_users_email', table_name='users')Add a foreign key constraint
def upgrade():
op.create_foreign_key(
'fk_posts_user_id_users',
source_table='posts',
referent_table='users',
local_cols=['user_id'],
remote_cols=['id'],
ondelete='CASCADE',
)
def downgrade():
op.drop_constraint('fk_posts_user_id_users', 'posts', type_='foreignkey')Execute raw SQL
Only when necessary, for example complex operations:
def upgrade():
op.execute("UPDATE users SET full_name = '' WHERE full_name IS NULL")
def downgrade():
# sometimes hard or impossible to fully reverse
passRunning Migrations
Once you have migration files, you apply them to the database.
Apply all pending migrations
alembic upgrade headThis will:
- Look at the current database revision.
- Run all
upgrade()functions up to the latest revision.
Upgrade to a specific revision
You can upgrade only to a particular revision ID:
alembic upgrade 1975ea83b712Alembic understands also relative steps:
# Go one revision forward
alembic upgrade +1
# Go three revisions forward
alembic upgrade +3Downgrade (rollback) migrations
To undo the last migration:
alembic downgrade -1To go all the way back to the base (no migrations):
alembic downgrade baseBe very careful when downgrading in production, because:
- You can lose data if a migration drops columns or tables.
- Downgrades are safe only if the downgrade code correctly reverses everything.
Check current revision
alembic currentThis prints the current revision of the connected database.
See history of revisions
alembic historyYou will see something like:
1975ea83b712 -> 3b1ae6349c66, add posts table
<base> -> 1975ea83b712, create initial tablesTypical Workflow with Alembic
Here is a practical step by step workflow for schema changes.
Scenario 1: Create initial schema
- Define your models using SQLAlchemy:
# app/models.py
from sqlalchemy import Column, Integer, String
from .db import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, nullable=False, index=True)
hashed_password = Column(String, nullable=False)- Initialize and configure Alembic, as described earlier.
- Autogenerate initial migration:
alembic revision --autogenerate -m "create users table"- Review the migration file, adjust if necessary.
- Apply the migration:
alembic upgrade head
Now the database has the users table.
Scenario 2: Add a new column
You decide to add is_active to User.
- Change the model:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, nullable=False, index=True)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, nullable=False, server_default="true")- Autogenerate:
alembic revision --autogenerate -m "add is_active to users"- Open the new migration:
def upgrade():
op.add_column(
'users',
sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False)
)
def downgrade():
op.drop_column('users', 'is_active')- Apply it:
alembic upgrade headNow all environments that run this migration will have the new column.
Scenario 3: Data migration with schema change
You need to split a full_name column into first_name and last_name.
- Update your models to use
first_nameandlast_name, and maybe keepfull_nametemporarily. - Manually write a migration that:
- Adds the new columns.
- Copies and splits data.
- (Optionally) drops the old column.
Example migration:
from alembic import op
import sqlalchemy as sa
revision = '6677b0f8e2c0'
down_revision = '3b1ae6349c66'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('users', sa.Column('first_name', sa.String(100), nullable=True))
op.add_column('users', sa.Column('last_name', sa.String(100), nullable=True))
# Example simple split: assume full_name is "First Last"
connection = op.get_bind()
users = connection.execute(sa.text("SELECT id, full_name FROM users")).fetchall()
for user in users:
full_name = user.full_name or ""
parts = full_name.split(" ", 1)
first = parts[0] if len(parts) >= 1 else ""
last = parts[1] if len(parts) == 2 else ""
connection.execute(
sa.text(
"UPDATE users SET first_name = :first, last_name = :last WHERE id = :id"
),
{"first": first, "last": last, "id": user.id},
)
# Optionally drop old column
op.drop_column('users', 'full_name')
def downgrade():
op.add_column('users', sa.Column('full_name', sa.String(200), nullable=True))
connection = op.get_bind()
users = connection.execute(
sa.text("SELECT id, first_name, last_name FROM users")
).fetchall()
for user in users:
full = (user.first_name or "") + " " + (user.last_name or "")
full = full.strip()
connection.execute(
sa.text(
"UPDATE users SET full_name = :full WHERE id = :id"
),
{"full": full, "id": user.id},
)
op.drop_column('users', 'first_name')
op.drop_column('users', 'last_name')This example shows that migrations are full Python scripts. You can read and write data directly when needed, not only alter schema.
Good Practices for Using Alembic
Keep migrations small and focused
Each revision should do one logical change, for example:
- Add a table.
- Add or change a few related columns.
- Add an index.
This makes upgrades and debugging easier.
Do not edit old migrations that already ran in shared environments
Once a migration has been:
- Committed to version control.
- Applied to any shared environment (staging, production).
Do not change its contents. Instead, create a new migration that fixes or adjusts the schema.
Changing old migrations after they ran can:
- Break new deployments that started from an older base.
- Confuse Alembic when revision code does not match the actual schema.
Always review autogenerated scripts
Autogeneration is helpful but not perfect. It can:
- Miss complex constraints.
- Generate wrong or unnecessary changes.
For example, if you only reorder columns in your model class, Alembic might try to recreate columns. That is not what you want. You must inspect and fix the resulting operations.
Use meaningful migration messages
When creating revisions:
alembic revision --autogenerate -m "add index on user email"
This message goes into the migration file and appears in alembic history. It is your documentation for the schema history.
Run migrations as part of deployment
A common production deployment flow:
- Build the new app image or package.
- Deploy it (but maybe keep it not serving traffic yet).
- Run
alembic upgrade headto update the database schema. - Start serving traffic with the new version.
This keeps app and database schema in sync.
Example: Integrating Alembic with a FastAPI + SQLAlchemy Project
Suppose you have:
app/
db.py
models.py
main.py
alembic/
env.py
versions/
alembic.ini
app/db.py:
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg2://user:pass@localhost/db")
engine = create_engine(DATABASE_URL, future=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()
app/models.py:
from sqlalchemy import Column, Integer, String, Boolean
from .db import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, nullable=False, server_default="true")
Configure alembic/env.py:
import os
import sys
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.db import Base, DATABASE_URL
config = context.config
fileConfig(config.config_file_name)
# Use DATABASE_URL from app
config.set_main_option("sqlalchemy.url", DATABASE_URL)
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()Workflow:
- Set
DATABASE_URLin your environment. - Generate initial migration:
alembic revision --autogenerate -m "create users table"- Review and run:
alembic upgrade head- Any time you change models, repeat:
alembic revision --autogenerate -m "describe your change"
alembic upgrade headSummary
- Alembic provides versioned migrations for SQLAlchemy based applications.
- You configure it via
alembic.iniandalembic/env.py, pointingtarget_metadatato yourBase.metadata. - Use
alembic revision --autogenerateto create migrations from model changes, then review and edit them. - Apply migrations with
alembic upgrade head, roll back withalembic downgrade. - Keep migrations small, do not modify old applied migrations, and make migrations part of your deployment process.
With a solid grasp of Alembic, you can evolve your database schema safely as your backend application grows.
Views: 6
KAHIBARO