This is Chapter 1 of the Alembic textbook. Alembic is the migration tool for SQLAlchemy. We cover setup, env.py configuration, and naming conventions.
Pairs with:
What Alembic does
Alembic generates and applies migrations: SQL/DDL changes that evolve your database schema over time.
v0: empty
v1: CREATE TABLE users
v2: ALTER TABLE users ADD COLUMN status
v3: CREATE INDEX ix_users_email
Each migration is a Python file with upgrade() and downgrade() functions.
Setup
uv add alembic
alembic init -t async migrations
Creates:
migrations/
├── env.py
├── README
├── script.py.mako
└── versions/
alembic.ini
-t async for async SQLAlchemy. Default: sync.
alembic.ini
[alembic]
script_location = migrations
sqlalchemy.url = postgresql+asyncpg://user:pass@host/db
For env-based config: keep URL out of ini; set from env.py.
env.py
# migrations/env.py
import asyncio
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from src.myapp.db import Base
from src.myapp.settings import settings
config = context.config
config.set_main_option("sqlalchemy.url", settings.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)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
connectable = async_engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
)
async with connectable.connect() as conn:
await conn.run_sync(do_run_migrations)
await connectable.dispose()
def do_run_migrations(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:
asyncio.run(run_migrations_online())
Async version. For sync: simpler env.py auto-generated.
Telling Alembic about your models
target_metadata = Base.metadata
But Base.metadata only knows about classes that have been imported. Make sure all model modules are imported before Alembic runs:
# Force import of all models
import src.myapp.models # imports User, Post, etc.
target_metadata = Base.metadata
Without this: autogenerate misses models.
Naming conventions
Set on Base.metadata:
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Without conventions, autogenerate produces names like ck_1. Hard to refer to in migrations or drops.
In env.py, when configuring context:
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
render_as_batch=False,
)
render_as_batch
For SQLite (which doesn’t support all ALTER TABLE):
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True, # for SQLite
)
Alembic emulates ALTER via temp-table-and-copy.
Comparing types
For autogenerate to detect type changes:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True, # detect type changes
compare_server_default=True, # detect server default changes
)
Without these, autogenerate misses type/default changes.
include_schemas
For multi-schema setups:
context.configure(
connection=connection,
target_metadata=target_metadata,
include_schemas=True,
)
Tells Alembic to include non-public schemas.
include_object filter
def include_object(object, name, type_, reflected, compare_to):
if type_ == "table" and name in IGNORE_TABLES:
return False
return True
context.configure(..., include_object=include_object)
For ignoring tables you don’t manage (e.g., third-party migrations).
script.py.mako
The template for new migrations. Default is fine; customize for organization-specific headers.
First migration
alembic revision --autogenerate -m "initial schema"
Creates migrations/versions/<rev>_initial_schema.py. Inspect; edit if needed.
alembic upgrade head
Applies. head = latest revision.
Status / history
alembic current # current rev
alembic history # revision list
alembic heads # all heads (multiple if branched)
alembic show <rev> # show migration content
Downgrade
alembic downgrade -1 # one step back
alembic downgrade <rev> # to specific
alembic downgrade base # all the way down
Useful in dev. Avoid in prod (forward-only).
What this textbook covers
| Ch | Topic |
|---|---|
| 1 | This chapter — intro + setup |
| 2 | Autogenerate: what it sees, what it misses |
| 3 | Manual migration writing |
| 4 | Branching and merging |
| 5 | Online schema changes (Postgres specifics) |
| 6 | Multi-database / multi-tenant migrations |
| 7 | CI integration |
| 8 | Production patterns and recovery |
Read this next
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .