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

ChTopic
1This chapter — intro + setup
2Autogenerate: what it sees, what it misses
3Manual migration writing
4Branching and merging
5Online schema changes (Postgres specifics)
6Multi-database / multi-tenant migrations
7CI integration
8Production 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 .