ORM models with Mapped and mapped_column

SQLAlchemy · Essentials · 12 min

Declarative models are how nearly all modern SQLAlchemy ORM code defines its data.

The ORM maps a class to a table and its instances to rows. In 2.x style you create one base class by subclassing DeclarativeBase, then each model subclasses that base and sets __tablename__.

Columns are declared with `Mapped[...]` type annotations, which drive both the Python typing and the database column:

  • id: Mapped[int] = mapped_column(primary_key=True) is an integer primary key.
  • name: Mapped[str] alone is enough for a NOT NULL string column.
  • Mapped[Optional[str]] or Mapped[str | None] makes the column nullable.
  • mapped_column(String(50), unique=True, index=True) adds a specific type and constraints.

Python types map to SQL types automatically (int to Integer, str to String, datetime to DateTime, Decimal to Numeric). Use server_default=func.now() for database-side defaults, or default= for Python-side ones.

Behind the scenes each class gets a Core Table, available as User.__table__, and the base has a shared Base.metadata, so Base.metadata.create_all(engine) works exactly as in Core.

Instances are plain objects: User(name="ada") uses a generated keyword constructor. They don't touch the database until added to a Session. Add a __repr__ for readable debugging output. Type checkers such as mypy understand Mapped[] natively, so user.name is known to be a str.

Example

from datetime import datetime
from typing import Optional
from sqlalchemy import String, func, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(String(120), unique=True)
    nickname: Mapped[Optional[str]]
    created: Mapped[datetime] = mapped_column(server_default=func.now())

    def __repr__(self) -> str:
        return f"User({self.id!r}, {self.email!r})"

engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
print(User.__table__.c.keys())  # ['id', 'email', 'nickname', 'created']
print(User(email="a@x.io"))     # User(None, 'a@x.io'): no id until flushed

Remember

  • Subclass DeclarativeBase once; every model inherits from it
  • Mapped[T] sets the Python type and nullability; mapped_column() adds SQL details
  • Optional[...] makes a column nullable, otherwise NOT NULL
  • Each model has a Core Table at Model.__table__

Watch out for

  • Expecting the id to be set right after User(...): it's assigned at flush.

Check yourself

  1. How do you declare a nullable text column `bio` in 2.0-style models?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sqlalchemy.org/en/20/orm/declarative_tables.html