Python Alembic Migrations: Revision, Upgrade, and Downgrade
python alembic migrations revision upgrade and downgrade: Learn how to create, apply, and revert Alembic migrations in Python using revision, upgrade, and downgrade co...
Managing python alembic migrations revision upgrade and downgrade is a daily task for any Python developer using SQLAlchemy. These three commands form the core lifecycle of schema changes: you generate a revision, apply it to a database, and roll it back when needed. This article explains how each command works, how Alembic tracks applied revisions, and what to watch for when running migrations in production.
The revision, upgrade, and downgrade cycle
Alembic treats database schema changes as a sequence of revisions. Each revision is a Python module that defines two functions: upgrade() and downgrade(). The upgrade() function applies the schema change, and downgrade() reverses it. The revision command creates these modules, upgrade executes them in order, and downgrade executes them in reverse order.
This cycle gives you a repeatable way to move a database forward or backward through its schema history. In a typical workflow you run alembic revision to create a new migration, alembic upgrade head to apply all pending migrations, and alembic downgrade -1 to revert the last one.
Creating a revision with alembic revision
The alembic revision command generates a new migration file. The most basic form is:
alembic revision -m "add users table"
This creates a file in your versions/ directory with a unique revision ID and a down_revision pointing to the current head. The file contains empty upgrade() and downgrade() functions that you fill in manually.
For most schema changes, you can use autogeneration to compare your SQLAlchemy model metadata against the current database state:
alembic revision --autogenerate -m "add email column"
Autogenerate inspects your models and the database, then writes the necessary operations into the migration file. For example, if you add a column to a model, the generated migration might look like:
# versions/abc123_add_email_column.py from alembic import op import sqlalchemy as sa revision = 'abc123' down_revision = 'def456' branch_labels = None depends_on = None def upgrade(): op.add_column('users', sa.Column('email', sa.String(length=255), nullable=True)) def downgrade(): op.drop_column('users', 'email')
Autogenerate is a starting point, not a guarantee. It does not detect every possible change, such as server-side defaults, table renames, or data transformations. Always review and edit the generated file before running it.
Applying migrations with alembic upgrade
The upgrade command applies pending revisions to the database. The most common invocation is alembic upgrade head, which brings the database to the latest revision:
alembic upgrade head
You can also upgrade to a specific revision by passing its ID:
alembic upgrade abc123
Or move forward by a relative number of steps:
alembic upgrade +2
When you run upgrade, Alembic reads the alembic_version table to determine the current revision, then executes the upgrade() function of every revision between the current state and the target, in dependency order. Each successful run updates the version table to the new revision ID.
Reverting schema changes with alembic downgrade
The downgrade command reverses migrations. The most common forms are:
alembic downgrade base
This reverts all migrations, leaving the database with no Alembic-managed schema. To revert to a specific revision:
alembic downgrade abc123
Or step back by a relative number:
alembic downgrade -1
downgrade executes the downgrade() functions in reverse order from the current revision down to the target. It is critical that each downgrade() correctly reverses the corresponding upgrade(). For destructive operations like dropping a table or removing a column, the downgrade may involve data loss, so it should be used deliberately.
How Alembic tracks applied revisions
Alembic stores the current revision in a table named alembic_version (by default). This table has a single row containing the revision ID of the last applied migration. Each revision file also declares its down_revision, forming a linear chain or a directed acyclic graph.
For example, a simple chain might look like:
base -> a1b2c3 -> d4e5f6 -> g7h8i9
If you run alembic upgrade head from an empty database, Alembic applies a1b2c3, then d4e5f6, then g7h8i9, updating the version table after each step. If you then run alembic downgrade -1, it reverts g7h8i9 and sets the version to d4e5f6.
Understanding this tracking mechanism helps you debug unexpected states. If a migration fails partway, the version table may not be updated, leaving the database in an inconsistent state. You can inspect the current revision with alembic current and the history with alembic history.
Handling multiple heads and branch merging
In a team environment, two developers may create revisions from the same parent, resulting in multiple heads. Alembic will refuse to run upgrade head until you resolve the branching by creating a merge revision.
To see all heads, use:
alembic heads
If you have two heads, say abc123 and def456, you can merge them with:
alembic merge -m "merge abc and def" abc123 def456
This creates a new revision whose down_revision is a tuple of both heads. The merge revision itself typically has empty upgrade() and downgrade() functions, but it tells Alembic that the branches have converged. After merging, upgrade head will apply both branches and then the merge revision.
Branching is a normal part of concurrent development, but it adds complexity. Keep branches short and merge frequently to reduce the risk of conflicting schema changes.
Production considerations for upgrade and downgrade
Running migrations in production requires planning beyond the basic commands. Schema changes can lock tables, cause downtime, and interact with live data. Consider these points before executing upgrade or downgrade on a production database.
Locking and downtime. Operations like add_column or create_index may lock the table for the duration of the migration. For large tables, this can block reads and writes. Some databases support online schema changes, but Alembic does not abstract that away. You may need to use database-specific options or run migrations during a maintenance window.
Data migration vs. schema migration. A schema change often requires transforming existing data. For example, adding a non-nullable column with a default might require a backfill. Autogenerate will not write data migrations for you. You must add the data manipulation logic inside the upgrade() function, often using SQLAlchemy core or raw SQL.
Idempotency and partial failures. Migrations are not automatically idempotent. If a migration fails halfway, you may need to manually fix the database state before retrying. In practice, you should test migrations on a staging database that mirrors production data volume and structure.
Downgrade safety. A downgrade is not always safe. Dropping a column or table destroys data. Before relying on downgrade as a rollback strategy, verify that the downgrade() function preserves or backs up any data you might need. In many cases, a forward-fix migration is safer than a downgrade.
Common pitfalls in autogenerated downgrade functions
Autogenerate is a powerful convenience, but it has blind spots that often surface in the downgrade() function. Because autogenerate only compares the current model metadata to the database, it may miss changes that are not reflected in the model, such as:
- Server-side defaults and database-level constraints
- Indexes created outside the model definition
- Triggers, views, or stored procedures
- Column type changes that are not automatically reversible
When autogenerate misses a change, the upgrade() may be incomplete, and the downgrade() may not reverse what upgrade() actually did. For example, if you add an index manually in the migration, autogenerate might not include the corresponding drop in downgrade(). Always inspect the generated migration and edit both functions to be symmetric.
Another common issue is relying on autogenerate for data transformations. Autogenerate only handles structural schema changes. If your migration moves data from one column to another, you must write that logic yourself, and the downgrade() must reverse it if you want a clean rollback.
A practical approach is to treat autogenerate as a draft. After running alembic revision --autogenerate, open the file, verify every operation, and add missing pieces. Test the migration on a copy of the database by running upgrade and then downgrade to ensure both directions work as expected.