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 aNOT NULLstring column.Mapped[Optional[str]]orMapped[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 flushedRemember
- 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.