Alembic and SQLModel: Database Migrations in Python

Alembic and SQLModel: Database Migrations in Python

8 min de leitura

If you’ve worked with databases in Python, you’ve probably felt the pain of managing schema changes. Add a column here, modify a type there, create an index somewhere else… and suddenly you no longer know what the actual state of your database is. Even worse: how do you ensure all environments (development, staging, production) are in sync?

The combination of SQLModel and Alembic solves this problem elegantly, bringing type safety with Pydantic, the robustness of SQLAlchemy, and version control for your database migrations. In this article, you’ll learn how to configure and use these two powerful tools together.

The Problem: Manual Schema and Chaos

In the traditional model, database changes are made manually via SQL scripts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Problematic approach - NEVER do this!
import sqlite3

def update_schema():
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    
    # What if this column already exists?
    cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)")
    
    # What if it fails halfway? How do you roll back?
    cursor.execute("ALTER TABLE users ADD COLUMN created_at TIMESTAMP")
    
    conn.commit()
    conn.close()

Problems with this approach:

No version control - You don’t know which changes have already been applied
No rollback - If something goes wrong, good luck undoing it manually
No validation - Errors only appear at runtime
Out-of-sync environments - Dev, staging, and production with different schemas
Complicated teamwork - How do you sync changes from multiple developers?

SQLModel: The Best of Both Worlds

SQLModel is a library created by Sebastian Ramirez (the same creator of FastAPI) that combines the best of Pydantic (type validation) with the best of SQLAlchemy (powerful ORM).

Why SQLModel?

Type Safety - Uses Python type hints for automatic validation
DRY (Don’t Repeat Yourself) - One model serves both validation and ORM
Editor Support - Autocomplete and type checking in your IDE
Compatible - Fully compatible with SQLAlchemy and Pydantic

Defining a SQLModel Model

See how simple it is to define a model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from typing import Optional
from sqlmodel import SQLModel, Field

class User(SQLModel, table=True):
    """User model in the database"""
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    age: Optional[int] = Field(default=None)
    active: bool = Field(default=True)

This model is simultaneously:

  • A Pydantic model for data validation
  • A SQLAlchemy model for database operations
  • A schema definition that can be versioned

Alembic: Version Control for Your Database

Alembic is a database migration tool for SQLAlchemy. Think of it as Git for your schema — it versions all changes and allows you to apply or revert them in a controlled way.

How Does It Work?

  1. You modify your model SQLModel (add a field, change a type, etc.)
  2. Alembic detects the change automatically by comparing the model with the database
  3. Generates a migration script with the necessary SQL operations
  4. You review and apply the migration in a controlled manner

Each migration has:

  • A unique ID (hash)
  • An upgrade() function to apply the change
  • A downgrade() function to revert the change
  • Reference to the previous migration (creating a chain of versions)

Hands On: Initial Setup

Let’s create a project from scratch and configure everything correctly.

1. Installation

1
pip install sqlmodel alembic

For PostgreSQL (production):

1
pip install psycopg2-binary

2. Project Structure

project/
├── app/
│   ├── __init__.py
│   ├── models.py      # SQLModel models
│   └── database.py    # Database configuration
├── alembic/
│   └── versions/      # Migrations go here (created automatically)
├── alembic.ini        # Alembic configuration
└── main.py            # Main application

3. Initializing Alembic

1
alembic init alembic

This creates the directory structure and configuration files.

4. Configuring Alembic for SQLModel

Step 1: Configure the database URL in alembic.ini:

1
2
3
4
5
6
7
8
9
# alembic.ini
[alembic]
# ... other configurations ...

# For SQLite (development)
sqlalchemy.url = sqlite:///./database.db

# For PostgreSQL (production)
# sqlalchemy.url = postgresql://user:password@localhost/dbname

Step 2: Configure env.py to use SQLModel metadata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# Import SQLModel and its models
from sqlmodel import SQLModel
from app.models import User  # Import ALL models here

config = context.config

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Configure SQLModel metadata for autogenerate
target_metadata = SQLModel.metadata

# ... rest of the code generated by Alembic ...

IMPORTANT: You need to import all your models in env.py, otherwise Alembic won’t detect them for autogenerate.

5. Create Initial Models

Create app/models.py:

1
2
3
4
5
6
7
8
9
from typing import Optional
from sqlmodel import SQLModel, Field

class User(SQLModel, table=True):
    """Represents a user in the system"""
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    active: bool = Field(default=True)

Create app/database.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sqlmodel import create_engine, SQLModel, Session

DATABASE_URL = "sqlite:///./database.db"

engine = create_engine(DATABASE_URL, echo=True)

def get_session():
    """Returns a database session"""
    with Session(engine) as session:
        yield session

First Migration: Creating the Initial Schema

Now let’s create our first migration with autogenerate — Alembic will automatically detect the models:

1
alembic revision --autogenerate -m "create users table"

You’ll see something like:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_name' on '['name']'
  Generating /project/alembic/versions/abc123_create_users_table.py ...  done

Always review the generated file! Open alembic/versions/abc123_create_users_table.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""create users table

Revision ID: abc123
Revises: 
Create Date: 2026-04-09 10:30:00.000000

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel

revision = 'abc123'
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
    """Apply changes"""
    op.create_table(
        'user',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(), nullable=False),
        sa.Column('email', sa.String(), nullable=False),
        sa.Column('active', sa.Boolean(), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
    op.create_index(op.f('ix_user_name'), 'user', ['name'], unique=False)


def downgrade() -> None:
    """Revert changes"""
    op.drop_index(op.f('ix_user_name'), table_name='user')
    op.drop_index(op.f('ix_user_email'), table_name='user')
    op.drop_table('user')

Now apply the migration:

1
alembic upgrade head

Output:

INFO  [alembic.runtime.migration] Running upgrade  -> abc123, create users table

Done! Your table was created.

Evolving the Schema: Adding Fields

Now let’s add new fields to our model. Imagine we need to store the creation date and SSN.

1. Modify the Model

Update app/models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from typing import Optional
from datetime import datetime
from sqlmodel import SQLModel, Field

class User(SQLModel, table=True):
    """Represents a user in the system"""
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    ssn: Optional[str] = Field(default=None, unique=True, max_length=11)  # NEW
    active: bool = Field(default=True)
    created_at: datetime = Field(default_factory=datetime.utcnow)  # NEW

2. Generate New Migration

1
alembic revision --autogenerate -m "add ssn and created_at"

Alembic automatically detects the changes:

INFO  [alembic.autogenerate.compare] Detected added column 'user.ssn'
INFO  [alembic.autogenerate.compare] Detected added column 'user.created_at'
  Generating /project/alembic/versions/def456_add_ssn_and_created_at.py ...  done

3. Review and Apply

1
alembic upgrade head

Relationships Between Tables

Let’s add a Post table related to User (one-to-many).

1. Define New Models

Update app/models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Optional, List
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    email: str = Field(unique=True, index=True)
    active: bool = Field(default=True)
    created_at: datetime = Field(default_factory=datetime.utcnow)
    posts: List["Post"] = Relationship(back_populates="author")


class Post(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str = Field(index=True)
    content: str
    published: bool = Field(default=False)
    created_at: datetime = Field(default_factory=datetime.utcnow)
    author_id: Optional[int] = Field(default=None, foreign_key="user.id")
    author: Optional[User] = Relationship(back_populates="posts")

2. Generate and Apply Migration

1
2
alembic revision --autogenerate -m "create posts table"
alembic upgrade head

Best Practices with Alembic and SQLModel

✅ Always Review Autogenerate Migrations

1
2
3
alembic revision --autogenerate -m "change xyz"
# REVIEW the file in alembic/versions/
alembic upgrade head

✅ Test Downgrades Locally

1
2
3
4
alembic upgrade head
alembic downgrade -1
alembic downgrade base
alembic upgrade head

✅ Use Descriptive Migration Names

1
2
3
4
5
# Bad
alembic revision --autogenerate -m "change"

# Good
alembic revision --autogenerate -m "add email_verified field to users table"

✅ Useful Alembic Commands

1
2
3
4
5
6
alembic history
alembic current
alembic upgrade head --sql
alembic upgrade abc123
alembic show abc123
alembic revision -m "custom migration"

Conclusion

The combination of SQLModel and Alembic offers a complete and elegant solution for working with databases in Python:

  • SQLModel brings type safety, automatic validation, and the simplicity of defining models just once
  • Alembic adds professional version control, automatic migrations, and the ability to evolve your schema safely

Main advantages of this stack:

Type Safety - Errors caught in the IDE, not in production
Version Control - Complete history of schema changes
Reliable Rollback - Revert changes when necessary
Intelligent Autogenerate - Alembic automatically detects changes
Teamwork - Sync schemas between developers
Consistent Environments - Dev, staging, and production always aligned

If you’re starting a new project or want to professionalize database management in an existing project, this combination is highly recommended.