The go-to resource for upgrading Python, Django, Flask, and your dependencies.

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 hintsFull typing
Legacy APIModern 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- Replace db.Column(Integer, primary_key=True) with mapped_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

ErrorCauseFix
TypeError: unbound methodSQLAlchemy 2.0 legacy APIUse model_class=Base; mapped_column
No type for Mapped[int]Missing importfrom sqlalchemy.orm import Mapped, mapped_column
Table name wrong__tablename__ unsetSet explicitly or trust auto
Alembic no detectModels not importedImport all models before env.py run_migrations_online
Queries failLegacy db.querydb.session.execute(select(Model))

Alembic & Production

  • flask db migrate (Flask-Migrate) / alembic revision --autogenerate
  • Test: pytest queries/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 shelldb.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