Cheatsheet for Alembic setup. Long-form: textbook .

Install + init

uv add alembic
alembic init -t async migrations         # async template
# or
alembic init migrations                  # sync

Layout:

migrations/
├── env.py
├── README
├── script.py.mako
└── versions/
alembic.ini

alembic.ini key settings

[alembic]
script_location = migrations
sqlalchemy.url =                # set from env.py
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
transaction_per_migration = false  # CONCURRENTLY ops won't be in transaction

env.py (async, SQLAlchemy 2.0)

import asyncio
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context

# Import models so Base.metadata is populated
import src.app.models  # noqa

from src.app.db import Base
from src.app.settings import settings

config = context.config
config.set_main_option("sqlalchemy.url", settings.database_url)

target_metadata = Base.metadata

def do_run_migrations(connection):
    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        compare_type=True,
        compare_server_default=True,
        include_schemas=False,
    )
    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()

if context.is_offline_mode():
    raise NotImplementedError("offline mode not supported")
else:
    asyncio.run(run_migrations_online())

env.py (sync)

from alembic import context
from sqlalchemy import engine_from_config, pool

from src.app.db import Base
import src.app.models  # noqa

config = context.config
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()

def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    with connectable.connect() as conn:
        context.configure(connection=conn, target_metadata=target_metadata, compare_type=True)
        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

Naming conventions (in 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 these: ck_1-style names.

URL from env

config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])

Or from Settings (preferred).

Verify setup

alembic current        # current revision
alembic heads          # head revisions
alembic history        # all revisions

First migration

alembic revision --autogenerate -m "initial schema"
# review the file
alembic upgrade head

Common mistakes

  • Models not imported in env.py — empty autogenerate.
  • compare_type=False (default) — type changes missed.
  • Sync env.py for async engine — fails.
  • Hard-coded URL in alembic.ini instead of from env.

Read this next

If you want my async env.py + naming convention template, it’s at rajpoot.dev .


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 .