WriteOnlyMapped: default_factory=list broken in 2.1 due to new declarative_scan validation · Issue #13227 · sqlalchemy/sqlalchemy · GitHub
Skip to content

WriteOnlyMapped: default_factory=list broken in 2.1 due to new declarative_scan validation #13227

@rhynzler

Description

@rhynzler

Describe the bug

The new default_factory validation introduced in
commit 95ad9fe
(related to #12168) breaks WriteOnlyMapped relationships that use default_factory=list
in Declarative Dataclass Mapping.

Optional link from https://docs.sqlalchemy.org which documents the behavior that is expected

No response

SQLAlchemy Version in Use

2.1.0b1

DBAPI (i.e. the database driver)

asyncpg

Database Vendor and Major Version

PostgreSQL 18

Python Version

3.14.2

Operating system

Linux

To Reproduce

from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, WriteOnlyMapped, mapped_column, relationship


class Base(DeclarativeBase, MappedAsDataclass):
    pass


class Parent(Base):
    __tablename__ = "parent"
    id: Mapped[int] = mapped_column(primary_key=True)

    children: WriteOnlyMapped["Child"] = relationship(
        back_populates="parent",
        default_factory=list,  # ← raises ArgumentError in 2.1
        cascade="all, delete-orphan",
        passive_deletes=True,
    )


class Child(Base):
    __tablename__ = "child"
    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(foreign_key="parent.id", ondelete="CASCADE")
    parent: Mapped[Parent] = relationship(back_populates="children")

Error

Traceback (most recent call last):
File "/home/.../project/sqla_relationship_bug.py", line 8, in
class Parent(Base):
...<8 lines>...
)
File "/home/.../project/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_api.py", line 867, in init_subclass
_ORMClassConfigurator._as_declarative(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
cls._sa_registry, cls, cls.dict
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/.../project/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 289, in as_declarative
return DeclarativeMapperConfig(registry, cls, dict
)
File "/home/.../project/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 1013, in init
self._extract_mappable_attributes()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/.../project/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 1590, in _extract_mappable_attributes
value.declarative_scan(
~~~~~~~~~~~~~~~~~~~~~~^
self,
^^^^^
...<7 lines>...
is_dataclass,
^^^^^^^^^^^^^
)
^
File "/home/.../project/.venv/lib/python3.14/site-packages/sqlalchemy/orm/relationships.py", line 1920, in declarative_scan
raise sa_exc.ArgumentError(
...<3 lines>...
)
sqlalchemy.exc.ArgumentError: For relationship Parent.children using dataclass options, default_factory must be exactly None

Additional context

Root Cause

The new validation in relationships.py (added in commit 95ad9fe) checks:

if (
    self._attribute_options.dataclasses_default_factory
    is not _NoArg.NO_ARG
    and self._attribute_options.dataclasses_default_factory
    is not self.collection_class  # ← None for WriteOnlyMapped
):
    raise sa_exc.ArgumentError(...)

For WriteOnlyMapped, collection_class is None because Write-Only collections
define their collection type through the annotation itself, not through collection_class.
This causes default_factory=list to always fail the is not self.collection_class check.

The same pattern is already correctly handled just a few lines above for the uselist check:

if (
    self.collection_class is None
    and not is_write_only   # ← already guards write-only here
    and not is_dynamic
):
    self.uselist = False

Proposed Fix

Add the same is_write_only and is_dynamic guards to the new validation:

if (
    self._attribute_options.dataclasses_default_factory
    is not _NoArg.NO_ARG
    and self._attribute_options.dataclasses_default_factory
    is not self.collection_class
    and not is_write_only   # ← add this
    and not is_dynamic      # ← add this
):
    raise sa_exc.ArgumentError(...)

Metadata

Metadata

Assignees

No one assigned

    Labels

    ormuse casenot really a feature or a bug; can be support for new DB features or user use cases not anticipated

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions