Typorithm · easy

Typed declarative models and database constraints

pythonsqlalchemyalembicdatabasesindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Typed declarative models and database constraints

0%

Concept and contract

Distinguish a mapped Python attribute, column nullability and a constraint enforced by the database.

A declarative model describes both a Python class and its relational table. DeclarativeBase owns a registry and metadata; Mapped marks attributes managed by the ORM; mapped_column provides explicit column details. These tools make the mapping understandable, but a Python annotation is not general runtime validation of every caller value.

The Role model has a generated primary key, a required title and an optional score. Its named CHECK requires a nonempty title at the database boundary. String(80) describes a SQL type; do not assume SQLite enforces that length like a Python validator.

Mechanism and worked example

The example creates only its owned schema, adds one Role inside a Session transaction and flushes it.

The example creates only its owned schema, adds one Role inside a Session transaction and flushes it. The returned report contains the stored values and the column’s actual nullable flag. With title QA and a missing score, the expected score is None and scoreNullable is True.

The optional annotation and the explicit nullable=True agree here. Explicit mapping configuration can override an inferred choice, so read both rather than trusting the annotation alone. The named CHECK is separate: an empty title reaches the database and is rejected with IntegrityError, even though its Python type is str.

Complete worked example

python

from sqlalchemy import CheckConstraint, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    __table_args__ = (CheckConstraint("length(title) > 0", name="ck_roles_title"),)
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(80))
    score: Mapped[int | None] = mapped_column(nullable=True)

def inspect_example(title, score):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            role = Role(title=title, score=score)
            session.add(role)
            session.flush()
            return {"title": role.title, "score": role.score,
                    "scoreNullable": Role.__table__.c.score.nullable}
    finally:
        engine.dispose()
Reasoning
The example creates only its owned schema, adds one Role inside a Session transaction and flushes it. The returned report contains the stored values and the column’s actual nullable flag. With title QA and a missing score, the expected score is None and scoreNullable is True. The optional annotation and the explicit nullable=True agree here. Explicit mapping configuration can override an inferred choice, so read both rather than trusting the annotation alone. The named CHECK is separate: an empty title reaches the database and is rejected with IntegrityError, even though its Python type is str.
Download the SQLAlchemy and Alembic computer lab ↗

Focused decision: context is in the complete example

python

score: Mapped[int | None] = mapped_column(nullable=True)
Database decision
The annotation permits None and the explicit column flag agrees. Changing the storage rule requires a constructor policy as well as a non-nullable column.
Download the SQLAlchemy and Alembic computer lab ↗

Counterexamples and common mistakes

The controlled bug makes score non-nullable without deciding how to handle a missing input.

The controlled bug makes score non-nullable without deciding how to handle a missing input. The insert then fails at the database instead of storing the documented optional value. Replacing the annotation alone would not provide a missing-value policy or make old nullable rows valid.

Do not describe Mapped[str] as a universal input validator, and do not confuse a client-side Python default with a database server_default. A constraint protects writes from every client that uses that database; a constructor convention protects only callers that follow that convention. Constraint names also make later migration review and error investigation clearer.

Controlled bug: observe the rejected database contract

python

from sqlalchemy import CheckConstraint, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    __table_args__ = (CheckConstraint("length(title) > 0", name="ck_roles_title"),)
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(80))
    score: Mapped[int | None] = mapped_column(nullable=False)

def inspect_example(title, score):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            role = Role(title=title, score=score)
            session.add(role)
            session.flush()
            return {"title": role.title, "score": role.score,
                    "scoreNullable": Role.__table__.c.score.nullable}
    finally:
        engine.dispose()
Reasoning
The controlled bug makes score non-nullable without deciding how to handle a missing input. The insert then fails at the database instead of storing the documented optional value. Replacing the annotation alone would not provide a missing-value policy or make old nullable rows valid. Do not describe Mapped[str] as a universal input validator, and do not confuse a client-side Python default with a database server_default. A constraint protects writes from every client that uses that database; a constructor convention protects only callers that follow that convention. Constraint names also make later migration review and error investigation clearer.
Download the SQLAlchemy and Alembic computer lab ↗

Independent practice

Type the optional score declaration first and explain why None is a meaningful value in this contract.

Type the optional score declaration first and explain why None is a meaningful value in this contract. The independent task changes the storage contract: score must become non-nullable, while a missing input is normalized to zero before insertion. Preserve explicit nonmissing values and the title constraint.

Make two small edits instead of redesigning the model: change the column setting and choose the value passed to Role. Run the missing-score case before checking a positive score. Retain the empty-title rejection so that solving one boundary does not silently weaken an unrelated database rule.

Step 1: Make the stored score non-nullable and normalize a missing input score to zero. Step 2: Preserve provided scores and the empty-title rejection.

Practice starter: solve before reading the solution

python

from sqlalchemy import CheckConstraint, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    __table_args__ = (CheckConstraint("length(title) > 0", name="ck_roles_title"),)
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(80))
    score: Mapped[int | None] = mapped_column(nullable=True)

def inspect_example(title, score):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            role = Role(title=title, score=score)
            session.add(role)
            session.flush()
            return {"title": role.title, "score": role.score,
                    "scoreNullable": Role.__table__.c.score.nullable}
    finally:
        engine.dispose()

# Task: Make the stored score non-nullable and normalize a missing input score to zero.
Reasoning
Make the stored score non-nullable and normalize a missing input score to zero.
Download the SQLAlchemy and Alembic computer lab ↗
  • Hint 1: Look at both the column declaration and the Role constructor call.
  • Hint 2: Use zero only when the input is None, not whenever it is falsey.

Solution, tests and trade-offs

The solution stores zero when the input score is None and reports scoreNullable as False.

The solution stores zero when the input score is None and reports scoreNullable as False. A provided score such as seven remains seven. Its acceptance cases also attempt an empty title and require the existing IntegrityError, demonstrating that the new policy preserves the old invariant.

This example creates a fresh table, so changing the Python declaration is enough for its disposable fixture. It is not enough for an existing deployed table. Later Alembic lessons separate a model change from the schema and data migration needed to make already stored rows satisfy that model.

Practice solution and acceptance checks

python

from sqlalchemy import CheckConstraint, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    __table_args__ = (CheckConstraint("length(title) > 0", name="ck_roles_title"),)
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(80))
    score: Mapped[int | None] = mapped_column(nullable=False)

def inspect_example(title, score):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            role = Role(title=title, score=0 if score is None else score)
            session.add(role)
            session.flush()
            return {"title": role.title, "score": role.score,
                    "scoreNullable": Role.__table__.c.score.nullable}
    finally:
        engine.dispose()
Reasoning
The solution stores zero when the input score is None and reports scoreNullable as False. A provided score such as seven remains seven. Its acceptance cases also attempt an empty title and require the existing IntegrityError, demonstrating that the new policy preserves the old invariant. This example creates a fresh table, so changing the Python declaration is enough for its disposable fixture. It is not enough for an existing deployed table. Later Alembic lessons separate a model change from the schema and data migration needed to make already stored rows satisfy that model.
Download the SQLAlchemy and Alembic computer lab ↗

Interview reasoning and next step

In an interview, trace a value from a function argument to a mapped attribute and then to a constrained column.

In an interview, trace a value from a function argument to a mapped attribute and then to a constrained column. Explain which layer rejects an empty title, which layer normalizes None, and how you would detect the actual database schema instead of guessing from source annotations.

SQLAlchemy 2.1 retains modern typed declarative mappings; the course avoids the legacy Query-centered API. Advanced mapped dataclass defaults have additional 2.1 behavior and are not assumed by these ordinary classes. Next, follow a pending object through autoflush, rollback and a fresh Session.

Optional stretch — after the core task works: Compare a Python default with an explicit server_default in another owned schema.

Computer practice: https://applydjinn.com/typorithm/sqlalchemy-lab-20261107.zip. Install the pinned requirements in an isolated Python 3.13 environment and run verify.py. Edit the working exercise baseline, then run --practice sa-mapping. The lab uses disposable SQLite databases and actual SQLAlchemy/Alembic calls; it does not connect to application accounts, production databases or paid services. The app includes prepared results and explained questions without an editor or Python runtime.

Distinguish a mapped Python attribute, column nullability and a constraint enforced by the database.