Flyway for Python: migrate versioned SQL with DBLift

Using Flyway with Python? Compare the workflows, keep versioned SQL, import migration history into DBLift, and handle Java migrations before switching.

You can use Flyway with a Python application: SQL migrations do not depend on the language of the app. Flyway also supports Python script migrations. If you are searching for a Flyway-style workflow installed with pip, DBLift runs versioned SQL and Python migration files and can import an existing Flyway history table.

This guide walks through that switch for Django, FastAPI and Flask projects: connection settings, migration history, pending Java code, and the deploy commands. For a new database, try the Python database migrations tutorial first.

If you write SQL files rather than ORM-generated diffs, this is for you; if Alembic autogenerate is your workflow, keep it.

Can Flyway run Python migrations?

Yes. Redgate documents support for .py files alongside other script migration formats. That documentation also notes that script migrations are not supported by Native Connectors. Check the connector you use before choosing that workflow.

DBLift's Python files use a different execution contract: a migrate(context) function receives a database handle, logging and configuration. A Flyway Python script is not automatically a DBLift migration just because both files end in .py. Adapt pending scripts to the MigrationContext API.

Keep Flyway when its current workflow fits your deployment. Consider DBLift when you want migration tooling installed in the same Python environment as your application, with versioned SQL and Python files tracked by one runner.

What carries over, and what does not

Start with your existing versioned SQL files, then check configuration, callbacks, placeholders and executable scripts against a disposable copy of the database. Importing history does not translate those behaviors.

FlywayDBLift
V1__create_users.sql, R__views.sql, U1__drop_users.sqlSame naming conventions; verify SQL and placeholder behavior before switching
CRC32 checksumsSame algorithm, so unchanged SQL still matches
flyway_schema_historyImported into dblift_schema_history
Java migrations on the classpathHistory is imported, the code does not run
flyway.conf and JVM flagsOne environment variable, or one YAML file

The double underscore in file names is still mandatory. The steps below cover the connection, history import and executable migration code.

Point it at a database

The shortest path is one environment variable. Every command in this post works once it is set.

pip install "dblift[postgresql]"
export DBLIFT_DB_URL="postgresql+psycopg://user:password@localhost:5432/mydb"
export DBLIFT_DB_SCHEMA="public"

Swap the extra for mysql, sqlserver, or oracle if that is your engine. Everything ships under one dblift package as of 4.0, so nothing it installs can collide with a module of your own.

Prefer a file? Put it in the project root. The CLI picks it up from the working directory when you pass neither a config path nor a URL.

database:
  type: postgresql
  url: "postgresql+psycopg://localhost:5432/app"
  schema: public
migrations:
  directories:
    - "./migrations"

Three things to know about that file:

  • The database block is required. The schema line is required for PostgreSQL, MySQL, SQL Server, and Oracle. SQLite and DuckDB have no schema to name.
  • Leave the migrations block out and DBLift reads a folder named migrations next to the file.
  • Credentials go in the environment (DBLIFT_DB_PASSWORD) or a secret URI, not in the file.

Out-of-order versions. The default, strict_mode: false, behaves like Flyway with outOfOrder=true: a V2 that lands after V3 is pending and runs. Set strict_mode: true and the same V2 fails instead, which is closer to Flyway's default. The flag form is --strict, accepted by every command except import-flyway.

Full reference: Configuration.

Bring the history with you

If the database already has a Flyway history table, import it. Do not baseline: baseline would collapse every applied version into a single row, and you would lose the per-version record Flyway built up.

dblift import-flyway   # copy the Flyway history table
dblift info            # applied vs pending, should match Flyway
dblift validate        # warns about scripts the folder no longer has

Both table names are defaults. Custom names take the --flyway-table and --table flags.

Every row in the Flyway table is translated on the way in:

Flyway row typeImported asRuns again?
SQLversioned SQLno
JDBC, SPRING_JDBCversioned SQLno
SCRIPTversioned SQLno
anything elsethe import stops—

Java rows are history, not code. A Flyway Java migration is a compiled class on the classpath, typically db.migration.V3__Foo. After import its version is marked applied, so migrate skips it. But there is no matching file in your migrations folder, and DBLift only executes .sql and .py. Validate warns about the missing file. Add --strict to turn the warning into an error, or run dblift repair to mark the script as deleted.

Any Java or script migration that is still pending has to be rewritten as V3__foo.sql or V3__foo.py before your first migrate. Importing history does not compile anything. Keep .java sources out of the migrations folder.

Step-by-step version: Move from Flyway.

Python when SQL is not enough

A Python migration uses the same file name as a SQL one, with a different extension. It exposes one function.

from dblift.api import MigrationContext


def migrate(context: MigrationContext) -> None:
    if context.dry_run:
        return
    context.execute("UPDATE accounts SET active = true WHERE active IS NULL")
  • Reach for Python when the change is data work or SDK calls that are painful in SQL. Relational engines accept both formats side by side.
  • Undo is a separate U1_0_0__….py with its own migrate function, not a second function in the same file.
  • Placeholders are not substituted in Python. Read them from context.placeholders.
  • Cosmos DB and MongoDB accept Python only. A .sql file there fails with DBLIFT-NOSQL-001 before anything runs.

Full contract: Python migrations.

Django, Flask, FastAPI

Django

pip install "dblift[django]"
INSTALLED_APPS = [
    # ...
    "dblift.integrations.django",
]
DBLIFT_MIGRATIONS_DIR = BASE_DIR / "migrations"

# Optional. Without these, DBLift uses DATABASES["default"].
# DBLIFT_DATABASE_ALIAS = "default"
# DBLIFT_DATABASE_URL = "postgresql+psycopg://user:pass@host/db"
python manage.py dblift_migrate
python manage.py dblift_validate
python manage.py dblift_info

This sits next to Django's ORM migrations rather than on top of them. DBLift keeps its own history table and never touches django_migrations. Keep both when you have tables outside the ORM, views, procedures, or reference data. The system check framework can warn when SQL is pending (check id dblift.W001), but a warning never applies anything. Apply stays a deploy step.

Flask and FastAPI

Flask gets a CLI command. Install the flask extra, call init_dblift(app, engine, "migrations") from dblift.integrations.flask with your SQLAlchemy engine, register the CLI, and flask dblift-migrate is available.

FastAPI helpers are read-only. migration_guard tells you whether the database is current; it will not migrate on startup.

Docs: Django · Flask · FastAPI

Tests and CI

The pytest plugin is its own package, not an extra. Its dblift_migrated_db fixture applies your migrations before the tests run.

pip install pytest-dblift

On GitHub, the Action installs the pip package and runs one command. It does not start a database for you.

- uses: dblift/action@v1
  with:
    command: migrate      # or validate, or info
    extras: postgresql

Docs: CI/CD

If you already know Flyway

Same SQL files, same checksum idea, history you import instead of throwing away. Config is one environment variable or one YAML file. Java classes come across as history, not as runnable code; SQL and Python do run. Django, Flask, pytest, and GitHub Actions are extras and a plugin, not a second toolchain.

DBLift is information technology / developer tools software. Contact: contact@dblift.com.