GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Dataclass inheritance pattern breaks with Python 3.14.1 · Issue #13021 · sqlalchemy/sqlalchemy

Dataclass inheritance pattern breaks with Python 3.14.1 #13021

New issue
Copy link
New issue
Copy link
Closed
Closed
Dataclass inheritance pattern breaks with Python 3.14.1#13021
Copy link
Labels
dataclassesexternal library/application issuesa separate library / application that's not SQLAlchemy has a problem (dependent or dependee)a separate library / application that's not SQLAlchemy has a problem (dependent or dependee)great mcveAn issue with a great mcveAn issue with a great mcve
Milestone
2.1

Description

@Giuzzilla
Giuzzilla
opened on Dec 3, 2025
Issue body actions

Describe the bug

After upgrading to the latest CPython patch release 3.14.1 my app started to break mentioning a KeyError on a field inherited from a Mixin.

Seems related to the discussion on CPython here: python/cpython#142214
Not sure if it's something that should be fixed by SQLAlchemy or by CPython.

See stacktrace below & MRE.

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

No response

SQLAlchemy Version in Use

2.0.44

DBAPI (i.e. the database driver)

/

Database Vendor and Major Version

/

Python Version

3.14.1

Operating system

Linux

To Reproduce

from sqlalchemy import ForeignKey
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    MappedAsDataclass,
    declared_attr,
    mapped_column,
    relationship,
)


class Base(MappedAsDataclass, DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]


class CreatedByMixin(MappedAsDataclass, kw_only=True):
    created_by_fk: Mapped[int] = mapped_column(
        ForeignKey("user_account.id"), init=False
    )

    @declared_attr
    @classmethod
    def created_by(cls) -> Mapped[User]:
        return relationship(foreign_keys=[cls.created_by_fk])


class Item(CreatedByMixin, Base, kw_only=True):
    __tablename__ = "item"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    description: Mapped[str]


class SpecialItem(Item, kw_only=True):
    __tablename__ = "special_item"

    id: Mapped[int] = mapped_column(ForeignKey("item.id"), init=False, primary_key=True)
    special_description: Mapped[str]

Error

Traceback (most recent call last):
  File "/home/myname/test/main.py", line 41, in <module>
    class SpecialItem(Item, kw_only=True):
    ...<3 lines>...
        special_description: Mapped[str]
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_api.py", line 626, in __init_subclass__
    super().__init_subclass__(**kw)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_api.py", line 846, in __init_subclass__
    _as_declarative(cls._sa_registry, cls, cls.__dict__)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 245, in _as_declarative
    return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 326, in setup_mapping
    return _ClassScanMapperConfig(
        registry, cls_, dict_, table, mapper_kw
    )
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 564, in __init__
    self._setup_dataclasses_transforms()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 1171, in _setup_dataclasses_transforms
    self._apply_dataclasses_to_any_class(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        dataclass_setup_arguments, self.cls, annotations
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/myname/test/.venv/lib/python3.14/site-packages/sqlalchemy/orm/decl_base.py", line 1227, in _apply_dataclasses_to_any_class
    dataclass_callable(  # type: ignore[call-overload]
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        klass,
        ^^^^^^
    ...<4 lines>...
        },
        ^^
    )
    ^
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/dataclasses.py", line 1436, in dataclass
    return wrap(cls)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/dataclasses.py", line 1426, in wrap
    return _process_class(cls, init, repr, eq, order, unsafe_hash,
                          frozen, match_args, kw_only, slots,
                          weakref_slot)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/dataclasses.py", line 1217, in _process_class
    text_sig = str(inspect.signature(
                   ~~~~~~~~~~~~~~~~~^
        cls,
        ^^^^
        annotation_format=annotationlib.Format.FORWARDREF,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )).replace(' -> None', '')
    ^
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 3321, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   globals=globals, locals=locals, eval_str=eval_str,
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   annotation_format=annotation_format)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 3036, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
                                    follow_wrapper_chains=follow_wrapped,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 2557, in _signature_from_callable
    return _get_signature_of(init)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 2440, in _signature_from_callable
    sig = _get_signature_of(obj.__func__)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 2511, in _signature_from_callable
    return _signature_from_function(sigcls, obj,
                                    skip_bound_arg=skip_bound_arg,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/inspect.py", line 2334, in _signature_from_function
    annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
                                  format=annotation_format)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/annotationlib.py", line 973, in get_annotations
    ann = _get_and_call_annotate(obj, format)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/annotationlib.py", line 1112, in _get_and_call_annotate
    ann = call_annotate_function(annotate, format, owner=obj)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/annotationlib.py", line 711, in call_annotate_function
    return annotate(format)
  File "/home/myname/.local/share/uv/python/cpython-3.14.1-linux-x86_64-gnu/lib/python3.14/dataclasses.py", line 553, in __annotate__
    new_annotations[k] = cls_annotations[k]
                         ~~~~~~~~~~~~~~~^^^
KeyError: 'created_by'

Additional context

It seems to happen only if I have the two-layers of inheritance (i.e. with SpecialItem inheriting from Item, doesn't happen with Item only).

Running the above MRE works fine with Python 3.14.0 and breaks with Python 3.14.1.

Activity

Next
  1. Giuzzilla
    added
    requires triageNew issue that requires categorizationNew issue that requires categorization
    on Dec 3, 2025
  2. CaselIT
    added
    out of scopebehavior or feature that we don't handle herebehavior or feature that we don't handle here
    and removed
    requires triageNew issue that requires categorizationNew issue that requires categorization
    on Dec 3, 2025
  3. CaselIT commented on Dec 3, 2025

    @CaselIT
    CaselIT
    on Dec 3, 2025
    Member
    More actions

    Hi,

    This seems to be a regression on python side.
    I'm not sure there is much that can be done in sqlalchemy side, since this a python bug. Let's see how the linked issue in python plays out
    Will try to reproduce without sqlalchemy.

  4. Giuzzilla commented on Dec 3, 2025

    @Giuzzilla
    Giuzzilla
    on Dec 3, 2025
    Author
    More actions

    Hi,

    This seems to be a regression on python side.
    I'm not sure there is much that can be done in sqlalchemy side, since this a python bug. Let's see how the linked issue in python plays out
    Will try to reproduce without sqlalchemy.

    Absolutely, I thought to open it here as well to keep track of it / in case there was something sketchy going on SQLA side and because I could reproduce it with SQLA (the linked CPython error seems slightly different even though it's still related to dataclasses).

  5. CaselIT commented on Dec 3, 2025

    @CaselIT
    CaselIT
    on Dec 3, 2025
    Member
    More actions

    Yes, thanks for opening, it's surely useful to be aware of these things.

  6. DavidCEllis commented on Dec 3, 2025

    @DavidCEllis
    DavidCEllis
    on Dec 3, 2025
    More actions

    Yes, apologies - this came as a side effect of an attempt to fix an issue with how dataclasses generates the annotations for its __init__ functions under PEP-649 annotations. Embedded in the __annotate__ logic was the mistaken assumption that if a field exists, it's because there is an annotation for that field. A tool replacing __annotations__ after the fact was not considered.

    There's a patch pending for this now: python/cpython#142223

  7. CaselIT commented on Dec 3, 2025

    @CaselIT
    CaselIT
    on Dec 3, 2025
    Member
    More actions

    @zzzeek since you have all the machinery to build python setup, could you test of that patch fixes things for us?

  8. zzzeek
    added
    dataclasses
    external library/application issuesa separate library / application that's not SQLAlchemy has a problem (dependent or dependee)a separate library / application that's not SQLAlchemy has a problem (dependent or dependee)
    great mcveAn issue with a great mcveAn issue with a great mcve
    on Dec 3, 2025
  9. sqla-tester commented on Dec 3, 2025

    @sqla-tester
    sqla-tester
    on Dec 3, 2025
    Collaborator
    More actions

    Mike Bayer has proposed a fix for this issue in the main branch:

    Add a test for #13021 https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/6505

  10. sqla-tester commented on Dec 3, 2025

    @sqla-tester
    sqla-tester
    on Dec 3, 2025
    Collaborator
    More actions

    Mike Bayer has proposed a fix for this issue in the rel_2_0 branch:

    Add a test for #13021 https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/6506

  11. zzzeek commented on Dec 3, 2025

    @zzzeek
    zzzeek
    on Dec 3, 2025
    Member
    More actions

    @zzzeek since you have all the machinery to build python setup, could you test of that patch fixes things for us?

    done python/cpython#142214 (comment)

  12. CaselIT commented on Dec 4, 2025

    @CaselIT
    CaselIT
    on Dec 4, 2025
    Member
    More actions

    Do we want to try using this suggestion in 3.14+? python/cpython#142214 (comment)

  13. 23 remaining items

  14. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    @DavidCEllis we've merged the above change where we dont have to swap out __annotations__ at all. looks like my original approach was overdoing it. nonethless, a public function that doesnt rely on class scan at all would be nice to have. thanks for the help!

  15. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    OK nope our __annotations__ change was merged prematurely, since it fails with this actual test case here that didnt get included in the CI runs. try again

  16. zzzeek
    reopened this on Dec 6, 2025
  17. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    this is quite the unique test case! for whatever reason. that's weird too since it does not look that interesting and we have TONS of mixin combination tests

  18. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    oh, but we dont haev them with kw_only=True since that's a new option. OKEY

  19. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    So the issue is that in the above test, "created_by" is not actually a valid keyword for SpecialItem, that is, this is invalid (this is my code, I know this is not part of the test):

    special_item = SpecialItem(
        special_description="sd1",
        description="d1",
        created_by=User(name="u1"),
    )
    
    

    in our current 2.0 release, the above code works. But with the change that simplifies everything, it no longer works. hence my unit test that did the above breaks.

    however pylance seems to agree with the new change:

    Image

    which seems to be because CreatedByMixin.created_by is not a dataclass property, it' s a declared_attr function, as it has to be in this case on a mixin. the only way to get type checking to work here is using a TYPE_CHECKING block.

    so now the issue is....is our dataclasses support supposed to also support @declared_attr callables as dataclass fields on init? with this change, it does not. did we have tests for this? docs?

  20. DavidCEllis commented on Dec 6, 2025

    @DavidCEllis
    DavidCEllis
    on Dec 6, 2025
    More actions

    I'll admit I don't really know what you're doing here - I haven't used the sqlalchemy dataclasses support personally.

    All I can say is the dataclasses issue arose because of the mistaken assumption I made that if a field exists, then it is because there is an annotation that created that field and so it was safe to retrieve that annotation.

    This example broke with 3.14.1 because the annotations you temporarily swap in have an extra entry compared to the original annotations. The extra entry makes an extra field, but then you remove the annotation.

    These are what you're swapping in and out:

    User
    use_annotations.keys() = dict_keys(['id', 'name'])
    restored.keys() = dict_keys(['id', 'name'])
    
    CreatedByMixin
    use_annotations.keys() = dict_keys(['created_by_fk'])
    restored.keys() = dict_keys(['created_by_fk'])
    
    Item
    use_annotations.keys() = dict_keys(['created_by_fk', 'id', 'description', 'created_by'])
    restored.keys() = dict_keys(['id', 'description'])
    
    SpecialItem
    use_annotations.keys() = dict_keys(['id', 'special_description'])
    restored.keys() = dict_keys(['id', 'special_description'])
    

    3.14.1 errored because created_by existed in the annotations while creating Item but doesn't while creating SpecialItem as it has been removed by then.

  21. Giuzzilla commented on Dec 6, 2025

    @Giuzzilla
    Giuzzilla
    on Dec 6, 2025
    Author
    More actions

    So the issue is that in the above test, "created_by" is not actually a valid keyword for SpecialItem, that is, this is invalid (this is my code, I know this is not part of the test):

    special_item = SpecialItem(
        special_description="sd1",
        description="d1",
        created_by=User(name="u1"),
    )
    

    in our current 2.0 release, the above code works. But with the change that simplifies everything, it no longer works. hence my unit test that did the above breaks.

    however pylance seems to agree with the new change:

    Image which seems to be because `CreatedByMixin.created_by` is not a dataclass property, it' s a `declared_attr` function, as it has to be in this case on a mixin. the only way to get type checking to work here is using a `TYPE_CHECKING ` block.

    so now the issue is....is our dataclasses support supposed to also support @declared_attr callables as dataclass fields on init? with this change, it does not. did we have tests for this? docs?

    Yeah, in my original code I indeed have an if TYPE_CHECKING: branch to please the type-checker.

    I think I saw this pattern first in a discussion here, e.g.: #9408

    My code has evolved over time and after 2.x I gradually moved every model to Dataclasses. (and this allowed me to keep the Mixins too)

  22. Giuzzilla commented on Dec 6, 2025

    @Giuzzilla
    Giuzzilla
    on Dec 6, 2025
    Author
    More actions

    oh, but we dont haev them with kw_only=True since that's a new option. OKEY

    The errors are identical if I remove the kw_only=True part, so I don't think that's relevant?

  23. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    kw_only is not relevant, my latest patch at https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/6510 has a full range of tests for using declared_attr with a dataclass

  24. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    I'll admit I don't really know what you're doing here - I haven't used the sqlalchemy dataclasses support personally.

    We need simply in some cases to make a dataclass that includes a field where the original class did not have an attribute annotation for that field; the reason it doesnt have one is because that attribute is actually coming from a superclass that has a class-level python descriptor (i.e. a descriptor that fires off when you access it as MyClass.attr). On the concrete inheriting class, we want that attribute to be a dataclass attribute.

    so at the moment there's no way to achieve this without overwriting __annotations__. my new patch is very careful about restoring __annotations__ this time around, however, it's not possible to restore __annotations__ if the original __annotations__ raised a NameError due to an import not resolvable. so i can't get it to work all the way, and I can't work around the Python 3.14.1 bug (though I still dont understand how dataclasses has an annotation dictionary with the key missing, when I'm setting new __annotations__ with the key; where did it get the old dictionary from?)

  25. DavidCEllis commented on Dec 6, 2025

    @DavidCEllis
    DavidCEllis
    on Dec 6, 2025
    More actions

    I believe what happens is this:

    • SQLAlchemy swaps in a dict with created_by to the annotations
    • Dataclasses uses this to create a field for created_by
    • SQLAlchemy removes the created_by value so it is no longer in the annotations when it tries to replace them with the originals
    • A subclass is created that will also have this field in __init__
    • When creating __doc__ inspect.signature is called on the dataclassses __init__ which in turn calls the __annotate__ function
    • This annotate function attempts to retrieve created_by from the inherited class annotations when called but it has been removed

    Unfortunately I think if you add a docstring some other logic still tries to inspect the __init__ function.

    In 3.14.2 this should just be skipped if the annotations can't be retrieved so the created_by will be unannotated on __init__.

  26. zzzeek commented on Dec 6, 2025

    @zzzeek
    zzzeek
    on Dec 6, 2025
    Member
    More actions

    why dont I just keep those new fields in __annotations__ then? since that's how dataclasses saw the class

  27. zzzeek commented on Dec 11, 2025

    @zzzeek
    zzzeek
    on Dec 11, 2025
    Member
    More actions

    I've implemented the fully clean __annotations__ swap pattern in ceeac6d - we will go forward not supporting the 3.14.1 case here, which is fixed in 3.14.2.

    Reacted by Giulio Mazzanti
    Reacted by Edgar Ramírez Mondragón
  28. zzzeek
    closed this as completedon Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dataclassesexternal library/application issuesa separate library / application that's not SQLAlchemy has a problem (dependent or dependee)a separate library / application that's not SQLAlchemy has a problem (dependent or dependee)great mcveAn issue with a great mcveAn issue with a great mcve

    Type

    No type

    Projects

    No projects

      Milestone

      • 2.1

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402520 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.