You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Struct fields aren't discovered in Python 3.14 #847
It looks like something is fundamentally broken in how Struct subclasses find their fields under 3.14b1. This was observed on Arm Mac with the latest release, and Linux with latest release as well as main.
Working, on 3.13:
[tim@tangerine ~]$ uvx --from='git+https://github.com/jcrist/msgspec@main' python
Built msgspec @ git+https://github.com/jcrist/msgspec@bc60e96772c5e8a3babff967d86a9e7dfcdbfb1b
Installed 1 package in 1ms
Python 3.13.2 (main, Feb 12 2025, 14:51:17) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from msgspec import Struct
>>> class S(Struct):
... rule_name: str
...
>>> S("foo")
S(rule_name='foo')
>>> S()
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
S()
~^^
TypeError: Missing required argument 'rule_name'
Broken, on 3.14:
[tim@tangerine ~]$ uvx -p 3.14 --from='git+https://github.com/jcrist/msgspec@main' python
Installed 1 package in 3ms
Python 3.14.0b1 (main, May 14 2025, 15:26:05) [GCC 15.1.1 20250425] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from msgspec import Struct
>>> class S(Struct):
... rule_name: str
...
>>> S("foo")
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
S("foo")
~^^^^^^^
TypeError: Extra positional arguments provided
>>> S()
S()
If that's the case, your code should work if you add a from __future__ import annotations to the top of the module, as that future directive is supposed to be kept in tact:
In Python 3.14, the behavior of code using from __future__ import annotations is unchanged.
I investigated a few options to see if this could be worked around without relying on from __future__ import annotations (as that's sufficient for a self-contained application code base, but not ideal for a library API that may receive struct instances defined elsewhere).
overriding __init__ in a subclass: disallowed by msgspec.Struct
overriding __init_subclass__ in a subclass: no apparent effect (metaclass is presumably trying to extract the fields before it runs __init_subclass__)
using a subclass of type(msgspec.Struct) as the metaclass: disallowed by type(msgspec.Struct) (the flag that allows subclassing is cleared)
The underlying issue comes from reading cls.__dict__["__annotations__"] rather than cls.__annotations__, so Python 3.14's backwards compatibility mechanism for the migration to lazy annotation evaluation as the default doesn't get a chance to trigger (the PR in #852 adds the now required lazy annotation evaluation logic to msgspec's own internal annotation lookup rather than changing the annotation lookup to go through the class descriptor machinery, since the latter would be a more intrusive change).
Edit: I had also tried __init_subclass__, but hadn't noted here that it didn't offer a workaround either
Activity
thatch commentedon May 14, 2025
Probably a dupe of #795 (but I skimmed over that one when searching, I didn't know this was annotations-related).
provinzkraut commentedon May 14, 2025
I'm assuming this might be because of PEP 649.
If that's the case, your code should work if you add a
from __future__ import annotationsto the top of the module, as that future directive is supposed to be kept in tact:thatch commentedon May 14, 2025
And tests pass!
ncoghlan commentedon Aug 19, 2025
Underlying issue is #651
Submitted PR is #852
ncoghlan commentedon Aug 22, 2025
I investigated a few options to see if this could be worked around without relying on
from __future__ import annotations(as that's sufficient for a self-contained application code base, but not ideal for a library API that may receive struct instances defined elsewhere).__init__in a subclass: disallowed bymsgspec.Struct__init_subclass__in a subclass: no apparent effect (metaclass is presumably trying to extract the fields before it runs__init_subclass__)type(msgspec.Struct)as the metaclass: disallowed bytype(msgspec.Struct)(the flag that allows subclassing is cleared)The underlying issue comes from reading
cls.__dict__["__annotations__"]rather thancls.__annotations__, so Python 3.14's backwards compatibility mechanism for the migration to lazy annotation evaluation as the default doesn't get a chance to trigger (the PR in #852 adds the now required lazy annotation evaluation logic tomsgspec's own internal annotation lookup rather than changing the annotation lookup to go through the class descriptor machinery, since the latter would be a more intrusive change).Edit: I had also tried
__init_subclass__, but hadn't noted here that it didn't offer a workaround either[fix:py3.14] Struct fields aren't discovered in Python 3.14
[fix:py3.14] Struct fields aren't discovered in Python 3.14
[fix:py3.14] Struct fields aren't discovered in Python 3.14
fix: Python 3.14 compatibility
fix: msgspec Python 3.14 compatibility (#6749)
ofek commentedon Oct 19, 2025
Resolved by #852
ofek commentedon Nov 24, 2025
https://github.com/jcrist/msgspec/releases/tag/0.20.0