Django ORM vs SQLAlchemy: Which Migrations Tool Wins?
The Problem with Database Migrations
Database migrations are unavoidable in web development, and choosing the wrong migration tool leads to lost productivity, team conflicts, and production disasters. I’ve worked with both Django ORM migrations and SQLAlchemy/Alembic, and each has its place depending on your project needs.
When you’re building a web application, your database schema will change. New features require new tables, existing tables need new columns, and relationships evolve over time. Without a reliable migration system, you’re stuck with manual schema management, which is error-prone and doesn’t scale across teams.
I’ve seen teams struggle with:
- Lost productivity from manual schema management
- Team conflicts from uncoordinated migrations
- Production disasters from misapplied changes
The good news is that both Django and SQLAlchemy have mature migration solutions. Let me show you how each works and help you decide which is right for your project.
Django ORM Migrations: Automatic and Integrated
If you’re already using Django, the built-in migration system is incredibly convenient. I love how zero-configuration it is - it works immediately after creating a Django project.
The typical workflow is straightforward:
python manage.py makemigrations # Auto-detects model changespython manage.py migrate # Applies changes to databaseLet me show you a practical example. I’ll create a simple Django model and see how migrations work:
from django.db import models
class Article(models.Model): title = models.CharField(max_length=200) content = models.TextField() published_at = models.DateTimeField(auto_now_add=True)
def __str__(self): return self.titleWhen I run makemigrations, Django automatically detects that I’ve created a new model:
$ python manage.py makemigrationsMigrations for 'blog': blog/migrations/0001_initial.py - Create model ArticleThe generated migration file is Python code that I can review:
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [ migrations.CreateModel( name='Article', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200)), ('content', models.TextField()), ('published_at', models.DateTimeField(auto_now_add=True)), ], ), ]One thing I appreciate about Django migrations is how they handle team conflicts. When two developers create migrations independently, Django detects the conflict and provides clear guidance:
$ python manage.py migrateCommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph.To fix this, run 'python manage.py makemigrations --merge'The --merge flag automatically creates a migration that combines both changes. I think this is one of Django’s strongest features - it makes collaboration almost effortless.
For rollbacks, I can simply migrate to a previous version:
$ python manage.py showmigrations blogblog [X] 0001_initial [X] 0002_add_author [X] 0003_add_tags
$ python manage.py migrate blog 0002_add_authorOperations to perform: Target specific migration: 0002_add_author, from blogRunning migrations: Rendering model states... DONE Unapplying blog.0003_add_tags... OKSQLAlchemy/Alembic: Flexible and Framework-Agnostic
When I’m working with Flask or FastAPI, or when I need database-agnostic migrations, SQLAlchemy with Alembic is my go-to choice. It requires more setup but offers greater flexibility.
The initial setup looks like this:
alembic init alembic # Create migration environment# Edit alembic.ini and env.py # Configure database connectionalembic revision --autogenerate # Generate migration (requires setup)alembic upgrade head # Apply migrationsLet me show you how I set up Alembic for a FastAPI project. First, I initialize the environment:
$ alembic init alembicCreating directory /path/to/alembic...doneCreating directory /path/to/alembic/versions...doneGenerating /path/to/alembic.ini...doneGenerating /path/to/alembic/env.py...doneGenerating /path/to/alembic/script.py.mako...doneGenerating /path/to/alembic/versions/README...done
Please edit configuration/connection/logging settings in'/path/to/alembic.ini' before proceeding.The most important file to configure is env.py. I need to import my SQLAlchemy models and set up the metadata:
from logging.config import fileConfigfrom sqlalchemy import engine_from_configfrom sqlalchemy import poolfrom alembic import contextimport sysimport os
# Add parent directory to pathsys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
# Import your models herefrom models import Base # This is your SQLAlchemy declarative base
config = context.config
if config.config_file_name is not None: fileConfig(config.config_file_name)
target_metadata = Base.metadata # This is crucial for autogenerate to workI also need to configure the database URL in alembic.ini:
[alembic]script_location = alembicsqlalchemy.url = postgresql://user:password@localhost/mydb
[post_write_hooks]
[loggers]keys = root,sqlalchemy,alembic
[handlers]keys = console
[formatters]keys = genericNow I can define my SQLAlchemy model:
from sqlalchemy import Column, Integer, String, DateTime, create_enginefrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.sql import func
Base = declarative_base()
class Article(Base): __tablename__ = 'articles'
id = Column(Integer, primary_key=True) title = Column(String(200)) content = Column(String) published_at = Column(DateTime, server_default=func.now())
def __repr__(self): return f"<Article(title='{self.title}')>"When I run alembic revision --autogenerate, Alembic detects the changes:
$ alembic revision --autogenerate -m "Initial migration"INFO [alembic.runtime.migration] Context impl PostgresqlImpl.INFO [alembic.runtime.migration] Context impl PostgresqlImpl.INFO [alembic.autogenerate.compare] Detected added table 'articles'Generating /path/to/alembic/versions/c3f8d1e2a4b6_initial_migration.py ... doneThe generated migration file gives me full control:
"""Initial migration
Revision ID: c3f8d1e2a4b6Revises:Create Date: 2026-04-09 10:00:00.000000
"""from alembic import opimport sqlalchemy as sa
# revision identifiers, used by Alembic.revision = 'c3f8d1e2a4b6'down_revision = Nonebranch_labels = Nonedepends_on = None
def upgrade(): op.create_table('articles', sa.Column('id', sa.Integer(), nullable=False), sa.Column('title', sa.String(length=200), nullable=True), sa.Column('content', sa.String(), nullable=True), sa.Column('published_at', sa.DateTime(), server_default=sa.func.now(), nullable=True), sa.PrimaryKeyConstraint('id') )
def downgrade(): op.drop_table('articles')What I really like about Alembic is the revision chain concept. Each migration has a down_revision that links it to the previous migration:
revision = 'a1b2c3d4e5f6'down_revision = 'c3f8d1e2a4b6' # Points to the previous migrationbranch_labels = Nonedepends_on = NoneThis allows for branching migrations in complex deployment scenarios - something that Django doesn’t easily support.
Common Mistakes I’ve Made
Django Mistakes
The biggest mistake I’ve made with Django is running makemigrations without reviewing what was detected:
$ python manage.py makemigrationsMigrations for 'blog': blog/migrations/0004_auto_20260409.py - Alter field title on article - Remove field author from articleI should always review the generated file before applying it, especially in production. Another mistake is creating unmergeable conflicts by running migrations in the wrong order across development and production environments.
Alembic Mistakes
The most common Alembic error I’ve seen is skipping the env.py configuration. When target_metadata isn’t set to your model’s metadata, --autogenerate won’t detect any changes:
# WRONG - autogenerate won't detect changestarget_metadata = None
# CORRECT - imports your modelsfrom models import Basetarget_metadata = Base.metadataAnother mistake is not understanding the revision chain. If you manually edit down_revision, you can break the migration graph and make rollbacks impossible.
Comparison Summary
| Feature | Django ORM | SQLAlchemy/Alembic |
|---|---|---|
| Setup Required | None | alembic init, config files |
| Auto-detection | Automatic from models | Requires --autogenerate + env.py setup |
| Framework Lock-in | Django only | Any SQLAlchemy project |
| Conflict Handling | Automatic detection | Manual merge commands |
| Learning Curve | Low (2 commands) | Medium (config + revision pattern) |
| Branching Support | Limited | Full support |
| Multiple Databases | Per-database config | Multiple configurations |
When to Choose What
I recommend Django ORM migrations when:
- You’re building a Django application
- Your team values simplicity and consistency
- You want zero configuration overhead
- You need automatic conflict resolution
I recommend SQLAlchemy/Alembic when:
- You’re using Flask, FastAPI, or other non-Django frameworks
- You need framework independence
- Your migration scenarios are complex (branching, multiple databases)
- You want full control over migration operations
One Reddit user I found summarized it well: “Django ORM and how easy to handle DB migrations are is why I’m having troubles finding a good reason to switch away from it.” This resonates with my experience - Django’s integration is hard to beat if you’re already in the Django ecosystem.
But for projects where flexibility matters, Alembic gives you the control you need. The setup cost is worth it when you’re working across multiple frameworks or need advanced migration features.
Summary
In this post, I compared Django ORM migrations and SQLAlchemy/Alembic. The key point is: if you’re already using Django, stick with Django migrations for their simplicity and automatic conflict resolution. If you’re using Flask, FastAPI, or need framework-independent migrations, Alembic gives you the control and flexibility you need.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Django Migrations Documentation
- 👨💻 Alembic Tutorial
- 👨💻 Reddit discussion: FastAPI vs Django (migration insights)
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments