← Back to list

Migrating aerich to Tortoise

In version 1.0.0, Tortoise ORM added a native migration tool that can replace Aerich or Alembic. If you’re anything like me, you’ll…

Conrad · 2026-04-14 08:48 · 6 claps · 3.9 min read
#fastapi #tortoise-orm #upgrades-and-migrations #aerich #python
Open on Medium ↗

Migrating aerich to Tortoise

In version 1.0.0, Tortoise ORM added a native migration tool that can replace Aerich or Alembic. If you’re anything like me, you’ll appreciate keeping dependencies to a minimum and having migrations handled directly by the ORM library. For smaller or new projects, you can mostly get away with just recreating the database —but for production systems this isn’t a viable option.

Please note that the Tortoise migration system is fairly new. Check the capabilities before you make the switch. In our projects we didn’t encounter any major problems, but you have been warned.

The aerich library was also developed by Tortoise and was the previous “native” approach.

So how do you actually migrate your migration system?

1. Preparation

Before proceeding, verify you have a current, tested backup. Also confirm that aerich upgrade and migrate run cleanly against all relevant databases.

Our high-level approach: keep the database state identical before and after. Any adjustments made outside your previous migration tool will be preserved. We tell Tortoise ORM that the initial migration already ran, even though it didn’t — similar to Django’s --fake flag, which Tortoise doesn't support yet.

Model Adjustments

As of v1.1.7, Tortoise’s migration system doesn’t support lambdas as default values. We worked around this by replacing them with named functions (see below). You may hit similar roadblocks on your first real migration — these were the only ones we encountered.

def empty_dict():
    return {}

class TestModel(BaseModel):
  attrs = JSONField(
    default=lambda: {}, # OLD
    default=empty_dict, # ADJUSTED
  )

2. The Migration

First bump Tortoise to any version above 1.0.0. You might have to remove aerich already due to version conflicts.

# When using uv
uv remove aerich
uv add "tortoise-orm>=1.0.0"
# When using pip
pip uninstall aerich
pip install "tortoise-orm>=1.0.0"

You should also remove the configuration section from your pyproject.yml. The dependencies section should be updated by uv, the lower section needs to be updated manually. If you are using pip, only the lower section will need updating, as well as your requirements.txt.

You will also have to update your TORTOISE_ORM variable to actually use the native migration system.

Now let’s create the first Tortoise based migration. This will be an empty migration for now, but we will add some content to it.

uv run tortoise makemigrations --empty # Create an empty migration file

aerich tracks migrations in its own table (aerich). The code below, pasted into the newly created 0001_initial.py, serves a dual purpose: it drops the old aerich table to avoid future confusion and creates Tortoise's own migration tracking table. If you'd rather keep the aerich table, just leave this migration empty.

from tortoise.migrations import Migration
from tortoise.migrations.operations import RunSQL

class Migration(Migration):
    initial = True

    operations = [
        RunSQL(
            sql="DROP TABLE IF EXISTS aerich;",
            reverse_sql="SELECT 1; -- irreversible migration",
        ),
    ]

Screenshot of the tortoise_migrations table

Screenshot of the tortoise_migrations table

If these changes are applied to all databases we have the new migration system in place. However at the moment the Tortoise migration system “thinks” the database is empty. Let’s change this.

uv run tortoise makemigrations -n transfer

Next, add 0002_transfer.py — this reflects the current database state. On new databases it will initialize correctly, but on production it will fail since the tables already exist. We need to tell Tortoise this migration is already applied. The easiest way is to run the following SQL directly against your production database(s):

INSERT INTO tortoise_migrations (app, name, applied_at) VALUES ('models', '0002_transfer', NOW());

If it is not possible or if there are too many production instances, you could also write a migration that does this automatically using a RunSQL command like before. In theory you could even add this to the first migration.

Now all production databases know that they are on the latest version of the migrations. You can now proceed to use tortoise makemigrationsand tortoise migrateas is.

3. Cleanup (optional)

If you want to clean up your repository and databases you could now manually drop the aerich database (if you didn’t use the Migration as stated).

Also you might want to delete the outdated aerich migrations folder.

4. Automigrate (optional)

I also needed to update my FastAPI lifecycle methods, since I apply migrations programmatically after the server starts. You can use the following function to handle migrations programmatically.

from tortoise.migrations.api import migrate

5. Conclusion

Depending on how you look at this this might not seem worth the time investment. That is a fair view I support. Never change a running system and everything still applies here.

I personally like to keep the library/ dependency count low in my projects, so for me it was a given once I read the changelog of Tortoise. Also I have no issue at all with deleting or squashing migration files after they are applied everywhere. “Loosing” the migration history is not a concern of mine. I worked with Django a long time and ever since switching to FastAPI I missed a lot of the tooling surrounding Django. This was the main reason I am using TortoiseORM with aerich instead of SQLAlchemy with alembic.


메타데이터
post_id
46bc3abd504e
slug
migrating-aerich-to-tortoise-46bc3abd504e
url
https://medium.com/@creyd/migrating-aerich-to-tortoise-46bc3abd504e
canonical_url
https://medium.com/@creyd/migrating-aerich-to-tortoise-46bc3abd504e
author_url
https://medium.com/@creyd
status
ok
fetched_at
2026-06-24 11:06:28