Skip to main content

migrify

Database migrations for Python: the autogenerate power of Alembic, the linear simplicity of Laravel.

Philosophy

Alembic is powerful but complex — branching, DAG graphs, multiple heads, down_revision chains.
Laravel migrations are simple — linear order by filename, a migrations table with a batch number, up() / down().

migrify takes the best of both:

Feature Source
Autogenerate from SQLAlchemy models Alembic
Full DDL operations API (op.*) Alembic
Multi-dialect support (PG, MySQL, SQLite...) Alembic
Linear ordering by timestamp filename Laravel
migrations (id, migration, batch) tracking table Laravel
Batch-based rollback Laravel
Simple migrate / rollback / fresh CLI Laravel

Installation

pip install migrify

Quickstart

1. Initialise (creates migrations/ and a ready-to-edit migrify.toml):

migrify init

Then open migrify.toml and set your db_url (use a sync driver):

db_url = "postgresql+psycopg2://user:pass@localhost/mydb"
migrations_dir = "migrations"
# models_module = "myapp.models"  # for autogenerate

Note: migrify is sync-only. If your app uses an async driver (e.g. asyncpg), migrify will auto-switch to the sync equivalent and print a warning.

2. Create a migration:

# Empty migration
migrify make create_users_table

# With autogenerate (compares your SQLAlchemy models with DB)
migrify make --autogenerate add_phone_to_users

Autogenerate requirement: the module set in models_module must expose a metadata attribute of type sqlalchemy.MetaData. With declarative models, add one line to your models package:

# myapp/models/__init__.py
from .base import Base          # your DeclarativeBase
# ... other imports ...

metadata = Base.metadata        # ← required for --autogenerate

The attribute name can be changed via models_metadata_attr in the config.

3. Edit the migration file:

# migrations/2024_01_15_143022_create_users_table.py
import sqlalchemy as sa
from migrify import op


def upgrade() -> None:
    op.create_table(
        "users",
        sa.Column("id", sa.Integer(), primary_key=True),
        sa.Column("email", sa.String(255), nullable=False),
        sa.Column("created_at", sa.DateTime(), nullable=True),
    )


def downgrade() -> None:
    op.drop_table("users")

4. Run migrations:

migrify migrate          # apply all pending
migrify status           # see what's applied
migrify rollback         # undo last batch
migrify fresh            # drop everything and re-migrate

CLI Reference

Command Description
migrify init Scaffold migrations/ dir and migrify.toml template
migrify migrate Apply all pending migrations
migrify migrate --step Apply pending, each in its own batch
migrify migrate --pretend Show SQL without executing
migrify rollback Rollback last batch
migrify rollback --batch 3 Rollback last 3 batches
migrify reset Rollback all migrations
migrify fresh Drop all tables + migrate
migrify status Show migration status
migrify make <name> Create empty migration
migrify make --autogenerate <name> Create migration from model diff

Configuration Reference

All options go in migrify.toml (or [tool.migrify] in pyproject.toml).

# ── Required ────────────────────────────────────────────────────────────────

# Database URL. Must use a sync driver.
# asyncpg / aiosqlite / aiomysql are auto-swapped to their sync equivalents.
db_url = "postgresql+psycopg2://user:pass@localhost/mydb"

# ── Optional ────────────────────────────────────────────────────────────────

# Directory where migration files are stored. Default: "migrations"
migrations_dir = "migrations"

# Name of the migrations tracking table. Default: "migrations"
migrations_table = "migrations"

# ── Autogenerate ─────────────────────────────────────────────────────────────

# Dotted Python path to the module exposing your SQLAlchemy MetaData.
# Required for `migrify make --autogenerate`.
models_module = "myapp.models"

# Attribute name of the MetaData inside models_module. Default: "metadata"
models_metadata_attr = "metadata"

# Whether to detect column type changes. Default: true
# Set to false if you have intentional model/DB type mismatches
# (e.g. Enum in model vs VARCHAR in DB — common when native_enum=False).
compare_types = true

# Tables to skip entirely during autogenerate comparison.
# Supports fnmatch patterns (*, ?, [seq]).
# Use for partition tables, legacy tables, or tables managed outside models.
exclude_tables = [
    "history_changes_p*",       # time-based partitions
    "history_changes_default",
    "alembic_version",          # if migrating away from Alembic
]

# Index names to skip during autogenerate comparison.
# Supports fnmatch patterns.
# Use for manually-created indexes (GIN, trigram, etc.) not defined in models.
# Tip: once you add an index to __table_args__ in your model, remove it here.
exclude_indexes = [
    "idx_products_*",
    "idx_groups_*",
]

# Columns to skip when detecting dropped columns. Format: "table.column".
# Use for columns that exist in DB but are not mapped in models
# (e.g. computed columns accessed via @property, or legacy columns).
exclude_columns = [
    "orders.extra_fee",
]

Autogenerate behaviour

migrify compares your SQLAlchemy models against the live database and generates the minimal set of DDL operations needed to bring the DB in sync with the models.

What is compared Default Notes
Missing / extra tables always controlled by exclude_tables
Missing / extra columns always controlled by exclude_columns
Column type changes compare_types = true disable for Enum↔VARCHAR mismatches
Column nullability always
Missing / extra indexes always controlled by exclude_indexes
Missing / extra foreign keys always matched by column signature, not name
Missing / extra unique constraints always

Indexes backed by UniqueConstraint or unique=True are automatically recognised and never generate spurious drop_index operations.

Functional indexes (e.g. GIN, tsvector) declared via text() in __table_args__ are rendered correctly with sa.literal_column(...).

Tracking Table

Unlike Alembic's alembic_version (which can have multiple rows for branches),
migrify uses a simple, always-linear migrations table:

CREATE TABLE migrations (
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    migration VARCHAR(255) NOT NULL,   -- filename without .py
    batch     INTEGER NOT NULL         -- group applied together
);

License

MIT

Release files for migrify 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for migrify 0.1.2
File Size Uploaded
migrify-0.1.2.tar.gz 35.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for migrify 0.1.2
File Interpreter ABI Platform
migrify-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 70.5 kB

Release files / migrify-0.1.2.tar.gz

Download URL migrify-0.1.2.tar.gz
Size 35.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b62e946003ede92e0592bf43f0315fd3dfc5a1494ff95da1e921f19f2fa6ca8e
BLAKE2b-256 checksum
How to use checksums
f2007c2dc5dcdfb9d4754f73c69a7fb7e2316f8da2d71da9cb978fe1c3a7e163
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.

Transparency log

Release files / migrify-0.1.2-py3-none-any.whl

Download URL migrify-0.1.2-py3-none-any.whl
Size 35.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7b9433b106626f6412848866ed739459bed1305013abdae89ac1adc4be5f29b
BLAKE2b-256 checksum
How to use checksums
f98e49c9411918e506cd4b976a44893084cbc110b9c5c4f1e4f4e7f17b534f59
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page