Alembic Setup — Initialising and Configuring env.py

Alembic is the database migration tool built specifically for SQLAlchemy. It tracks your schema’s history as a chain of versioned migration scripts, each with an upgrade() and downgrade() function. When the schema changes — a new column, a dropped index, a renamed table — you generate a migration script, commit it to version control, and run it against every environment. Alembic stores the current migration version in an alembic_version table and applies only the unapplied migrations, making the process idempotent and safe to run multiple times. Correct setup — especially pointing Alembic at your models — is the foundation for everything else in this chapter.

Installation and Initialisation

pip install alembic

# Initialise the migration environment (run once per project)
alembic init alembic

# Resulting directory structure:
# alembic/
#   env.py             ← edit this to connect to your DB and import models
#   script.py.mako     ← template for generated migration files
#   versions/          ← generated migration scripts live here
# alembic.ini          ← main config file (database URL configured here or in env.py)
Note: The alembic init alembic command creates a directory named alembic containing the migration environment. Some teams prefer to name this directory migrations for clarity: alembic init migrations. The directory name does not affect functionality — just update script_location in alembic.ini to match. The alembic/versions/ directory starts empty and fills with generated migration files as your schema evolves.
Tip: Commit the entire alembic/ directory (including env.py and versions/) to version control. Each migration script in versions/ is part of your project history — they document exactly how the schema evolved and are needed to recreate the database from scratch. Never delete migration files unless you are squashing migrations intentionally (merging old migrations into a single “initial state” baseline).
Warning: The database URL in alembic.ini is committed to version control. Never put real credentials there. The correct approach is to keep alembic.ini‘s sqlalchemy.url as a placeholder and override it dynamically in env.py from an environment variable. This way the same migration code works across all environments without credentials ever being in source control.

Configuring env.py

# alembic/env.py — the critical configuration file
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# ── Import EVERYTHING that inherits from Base ─────────────────────────────────
# Alembic's autogenerate compares Base.metadata to the current DB schema.
# If a model is not imported here, Alembic cannot see it → migration misses it.
from app.database import Base       # the DeclarativeBase
from app.models.user import User    # import every model
from app.models.post import Post
from app.models.tag  import Tag
from app.models.comment import Comment
# Or: from app.models import *  (if models/__init__.py imports all models)

# ── Read database URL from environment (not from alembic.ini) ────────────────
from app.config import get_settings
settings = get_settings()

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

# ── Standard Alembic boilerplate ──────────────────────────────────────────────
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata = Base.metadata   # tells autogenerate what to compare against

def run_migrations_offline() -> None:
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url              = url,
        target_metadata  = target_metadata,
        literal_binds    = True,
        dialect_opts     = {"paramstyle": "named"},
        compare_type     = True,   # detect column type changes
        compare_server_default = True,  # detect server_default changes
    )
    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online() -> None:
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix      = "sqlalchemy.",
        poolclass   = pool.NullPool,   # no pool for migration runs
    )
    with connectable.connect() as connection:
        context.configure(
            connection       = connection,
            target_metadata  = target_metadata,
            compare_type     = True,
            compare_server_default = True,
        )
        with context.begin_transaction():
            context.run_migrations()

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

Verifying the Setup

# Check current migration state
alembic current
# If database has no alembic_version table: "(head)" after running first migration
# If no migrations applied yet: outputs nothing or "No current revisions"

# Show all migrations and their status
alembic history --verbose

# Verify autogenerate can see your models (dry run — no file written)
alembic revision --autogenerate -m "test" --sql   # shows SQL without writing a file

# Check for any differences between models and DB (useful in CI)
# Returns non-zero exit code if migrations are needed
alembic check   # Alembic 1.9+

Common Mistakes

Mistake 1 — Not importing all models in env.py

❌ Wrong — model not imported, Alembic does not see it:

from app.database import Base
from app.models.user import User   # only User imported
# Post, Tag, Comment not imported → their tables missing from autogenerate!

✅ Correct — import every model:

from app.database import Base
from app.models.user    import User
from app.models.post    import Post
from app.models.tag     import Tag
from app.models.comment import Comment   # ✓ all models imported

Mistake 2 — Hardcoding database URL in alembic.ini

❌ Wrong — credentials in version control:

# alembic.ini
sqlalchemy.url = postgresql://user:secret@prod-db/mydb   # EXPOSED!

✅ Correct — override in env.py from environment variable.

Mistake 3 — Forgetting compare_type=True (type changes not detected)

❌ Wrong — column type changes silently ignored:

context.configure(connection=connection, target_metadata=target_metadata)
# compare_type defaults to False — VARCHAR→TEXT changes not detected!

✅ Correct:

context.configure(connection=connection, target_metadata=target_metadata,
                  compare_type=True)   # ✓ detects column type changes

Quick Reference

Task Command / Code
Initialise alembic init alembic
Current version alembic current
Migration history alembic history --verbose
Point at models target_metadata = Base.metadata in env.py
Read URL from env config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
Check for drift alembic check (Alembic 1.9+)

🧠 Test Yourself

You add a new Comment model in app/models/comment.py but forget to import it in alembic/env.py. You run alembic revision --autogenerate -m "add_comments". What happens?