Alembic — Quick Reference
What Is Alembic?
Alembic is Prisma Migrate for Python/SQLAlchemy. You change your models → Alembic generates the SQL → you apply it to the DB.
The core problem it solves: your Python model changed, but the real database has no idea. Alembic bridges that gap with versioned, reproducible migration files.
Direct Mapping — Prisma / Drizzle vs Alembic
| Concept | Prisma | Drizzle | Alembic |
|---|---|---|---|
| Define schema | schema.prisma | schema.ts | SQLAlchemy models (models/) |
| Generate migration | prisma migrate dev | drizzle-kit generate | alembic revision --autogenerate |
| Apply migration | prisma migrate deploy | drizzle-kit push | alembic upgrade head |
| Migration files | migrations/ | migrations/ | alembic/versions/ |
| Roll back | ❌ not built-in | ❌ not built-in | ✅ alembic downgrade -1 |
Alembic's rollback is a genuine advantage over Prisma/Drizzle. Every migration has an
upgrade()anddowngrade()— you can go backwards cleanly.
Key Difference vs Prisma
In Prisma, the .prisma file is the single source of truth — it generates both the client and migrations.
In SQLAlchemy + Alembic, they are two separate tools:
SQLAlchemy models → your Python source of truth (defines classes)
Alembic → watches those models, generates SQL migrationsYou have to manually wire them together in alembic/env.py.
Setup
pip install alembic
alembic init alembic # creates alembic/ folder + alembic.inialembic.ini
sqlalchemy.url = postgresql+asyncpg://user:password@localhost/mydbalembic/env.py — The Wiring File
from app.database import Base
from app.models import user, post, tag # ← import ALL models here
# Alembic can only see what's imported
target_metadata = Base.metadata⚠️ #1 Gotcha: If you add a new model file and forget to import it here, Alembic will not detect it — and won't generate a migration for it.
The Workflow (3 Steps Every Time)
Step 1 — Change your model
# Added two new columns to User
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255))
bio: Mapped[str | None] = mapped_column(Text, nullable=True) # ← new
avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # ← newStep 2 — Generate the migration
alembic revision --autogenerate -m "add bio and avatar to users"Alembic compares your current models vs what the DB looks like right now and generates a migration file automatically:
# alembic/versions/abc123_add_bio_and_avatar_to_users.py
def upgrade():
op.add_column("users", sa.Column("bio", sa.Text(), nullable=True))
op.add_column("users", sa.Column("avatar_url", sa.String(500), nullable=True))
def downgrade():
op.drop_column("users", "avatar_url")
op.drop_column("users", "bio")Step 3 — Apply it
alembic upgrade head # "head" = latest migrationCommon Commands
# Apply all pending migrations
alembic upgrade head
# Roll back the last migration
alembic downgrade -1
# Roll back to a specific version
alembic downgrade abc123
# See full migration history
alembic history
# See where your DB currently is
alembic current
# Generate a blank migration (for manual SQL)
alembic revision -m "some manual change"Base.metadata.create_all vs Alembic
create_all | Alembic | |
|---|---|---|
| Use case | Local dev only | Staging + Production |
| Tracks changes | ❌ No | ✅ Yes, versioned |
| Safe on existing data | ❌ Won't alter existing tables | ✅ Generates precise ALTER statements |
| Rollback | ❌ No | ✅ Yes, via downgrade() |
Rule of thumb: use
create_allto spin up a fresh local DB fast. Use Alembic for anything that has real data.
What a Migration File Looks Like
# alembic/versions/abc123_add_role_to_users.py
revision = "abc123"
down_revision = "xyz789" # ← points to the previous migration (forms a chain)
branch_labels = None
depends_on = None
def upgrade():
op.add_column("users", sa.Column("role", sa.String(50), nullable=True))
def downgrade():
op.drop_column("users", "role")
# ⚠️ downgrade drops the column — all data in that column is permanently lostAlways write a proper
downgrade(). It's what makes rollbacks possible.
Gotchas
| Gotcha | Fix |
|---|---|
| New model not detected | Import it in alembic/env.py |
--autogenerate misses some changes | Always review generated files before applying |
| Rolling back drops columns | Data in those columns is gone — back up first |
Running create_all alongside Alembic | Don't mix them — pick one approach per environment |