Flask-SQLAlchemy 3.1: Migrating from db.Model to Declarative Base Syntax
Flask-SQLAlchemy 3.1 brings support for SQLAlchemy 2.0’s modern declarative mapping style. If you’ve upgraded and started seeing TypeErrors with your db.Model classes, migrating to DeclarativeBase, Mapped, and mapped_column will resolve them while opening up better typing and tooling.
Migrating offers practical advantages—including type hints compatible with mypy and Pyright, improved IDE autocompletion, smoother Alembic autogeneration, and better positioning for future SQLAlchemy changes—though your existing models will continue to function.
Why Migrate Now?
SQLAlchemy evolved from classic mappings in version 1.x—where models inherited directly and defined columns imperatively—to a unified declarative style in 2.0 that emphasizes type annotations for better static analysis and tooling.
| Legacy (db.Model) | New (DeclarativeBase) |
|---|---|
class User(db.Model): id = db.Column(...) | class User(db.Model): id: Mapped[int] = mapped_column(...) |
| No type hints | Full typing |
| Legacy API | Modern 2.0+ |
Flask-SQLAlchemy 3.1 supports this evolution through model_class=Base. The legacy syntax remains backward compatible—your app won’t break on upgrade—but we’ll benefit from type hints, IDE intelligence, and Alembic improvements over time, especially in larger codebases where maintenance costs add up.
Prerequisites: Upgrade Dependencies
pip install --upgrade "Flask-SQLAlchemy>=3.1.1" "SQLAlchemy>=2.0.16"
# Verify
pip show flask-sqlalchemy sqlalchemy
After upgrading, test that your app runs unchanged—this confirms backward compatibility.
Old vs New Model Syntax
Your legacy models.py (before):
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120))
Updated models.py (after):
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String
app = Flask(__name__)
class Base(DeclarativeBase):
pass
db = SQLAlchemy(app, model_class=Base)
class User(db.Model):
__tablename__ = 'user' # Optional: auto-generates 'user'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True)
email: Mapped[str] = mapped_column(String(120))
Notice how db.Model now uses our Base—and mapped_column pulls type information and defaults from the Mapped annotation.
Step-by-Step Migration
1. Define Base Class
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass # Customize: metadata=MetaData(naming_convention=...)
2. Init db with model_class
db = SQLAlchemy(app, model_class=Base)
3. Update Models
- Annotate each field, e.g.,
id: Mapped[int]\n- Replacedb.Column(Integer, primary_key=True)withmapped_column(Integer, primary_key=True)\n- Add__tablename__ = 'user'if the auto-generated name (snake_case from class) doesn’t fit\n\nYou can update models gradually—legacy and declarative styles mix during transition.
4. Updating Queries (Optional)\nOf course, legacy methods like User.query.all() continue to work. For new queries, though, consider SQLAlchemy 2.0’s select():\nNew:
from sqlalchemy import select
users = db.session.execute(select(User)).scalars().all()
# Or: db.paginate(select(User), page=1, per_page=10)
5. Create Tables / Migrate
with app.app_context():
db.create_all() # Dev only
Alembic: alembic revision --autogenerate detects changes.
Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
TypeError: unbound method | SQLAlchemy 2.0 legacy API | Use model_class=Base; mapped_column |
No type for Mapped[int] | Missing import | from sqlalchemy.orm import Mapped, mapped_column |
| Table name wrong | __tablename__ unset | Set explicitly or trust auto |
| Alembic no detect | Models not imported | Import all models before env.py run_migrations_online |
| Queries fail | Legacy db.query | db.session.execute(select(Model)) |
Alembic & Production
flask db migrate(Flask-Migrate) /alembic revision --autogenerate- Test:
pytestqueries/migrations. - Pin:
flask-sqlalchemy==3.1.1 sqlalchemy==2.0.36
Verify with mypy models.py; expect improved Pyright and VS Code autocomplete.
Conclusion
In summary, we’ll define a DeclarativeBase subclass, initialize db with model_class=Base, and refactor models to Mapped and mapped_column. This supports gradual adoption with no downtime and adds type checking benefits for maintainable codebases.
Related:
Test: Update models.py, flask shell → db.create_all().
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming