Repository navigation
surprising new behavior in inspect/annotationlib get_annotations as of 3.14.0b1, not in 3.14.0a7. intended change? #133684
Copy link
Copy link
Closed
Labels
3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
3.14.0b1 seems to be interpreting the annotations of a metaclass into a new format that is also interfering with the correct annotations of subclasses of that class. The subclass reports the annotations of the parent class, if it does not itself have any annotations. this only occurs if superclass is from a custom metaclass. if the subclass does have annotations, then the superclass annotations are not reported. this behavior is inconsistent with previous python versions including 3.14.0a7 and is also inconsistent with itself. see script below
from __future__ import annotations
import inspect
# a metaclass with an annotation
class MyMetaClass(type):
metadata: dict[str, str]
# dynamic class based on metaclass
MyNewClass = MyMetaClass("MyNewClass", (object,), {"metadata": {}})
# this seems new, but maybe intentional. annotations are shown for the
# dynamic class that come from the metaclass. OK
# 3.13.3: {}
# 3.14.0a7: {}
# 3.14.0b1: {'metadata': 'dict[str, str]'}
print(inspect.get_annotations(MyNewClass))
# declared class based on metaclass. Add another anno to it.
class MyOtherNewClass(metaclass=MyMetaClass):
metadata: dict[str, str] = {}
someint: int
# both pythons look good and this agrees with older pythons too.
# 3.13.3: {'metadata': 'dict[str, str]', 'someint': 'int'}
# 3.14.0a7: {'metadata': 'dict[str, str]', 'someint': 'int'}
# 3.14.0b1: {'metadata': 'dict[str, str]', 'someint': 'int'}
print(inspect.get_annotations(MyOtherNewClass))
# a control, a normal class without special metaclass.
class YetAnotherClass:
metadata: dict[str, str] = {}
someint: int
# here's where it goes wrong. If we make new classes from these
# bases that have *no* annotations, the *superclass annotations leak into the subclass*
class MySubClass(MyNewClass):
pass
# 3.13.3: {}
# 3.14.0a7: {}
# 3.14.0b1: {'metadata': 'dict[str, str]', 'someint': 'int'} # this has to be wrong
print(inspect.get_annotations(MySubClass))
# if we add *any* annotations to MySubClass, the above annos *disappear*
class MySubClass(MyNewClass):
foobar: float
# these look all correct. but 3.14.0b1's seems to be inconsistent with itself
# 3.13.3: {'foobar': 'float'}
# 3.14.0a7: {'foobar': 'float'}
# 3.14.0b1: {'foobar': 'float'} # wait what? What happened to metadata/someint from above?
print(inspect.get_annotations(MySubClass))
class MyOtherSubClass(MyOtherNewClass):
pass
# similar behaviors for declared class
# 3.13.3: {}
# 3.14.0a7: {}
# 3.14.0b1: {'metadata': 'dict[str, str]', 'someint': 'int'}
print(inspect.get_annotations(MyOtherSubClass))
# behavior does not seem to occur at all without a metaclass.
# YetAnotherClass has annotations but we do not see them from subclasses
# of that class
class MyYetAnotherSubClass(YetAnotherClass):
pass
# 3.13.3: {}
# 3.14.0a7: {}
# 3.14.0b1: {}
print(inspect.get_annotations(MyYetAnotherSubClass))outputs:
$ python test4.py # python 3.13.3
{}
{'metadata': 'dict[str, str]', 'someint': 'int'}
{}
{'foobar': 'float'}
{}
{}
$ ~/.venv314a7/bin/python test4.py # python 3.14.0a7
{}
{'metadata': 'dict[str, str]', 'someint': 'int'}
{}
{'foobar': 'float'}
{}
{}
$ ~/.venv314/bin/python test4.py # python 3.14.0b1
{'metadata': 'dict[str, str]'}
{'metadata': 'dict[str, str]', 'someint': 'int'}
{'metadata': 'dict[str, str]'}
{'foobar': 'float'}
{'metadata': 'dict[str, str]', 'someint': 'int'}
{}
CPython versions tested on:
3.14
Operating systems tested on:
No response
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.14bugs and security fixesbugs and security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
zzzeek commentedon May 8, 2025
cc @JelleZijlstra
zzzeek commentedon May 8, 2025
further issues that seem related to the same thing
make two files, one with
from __future__ import annotations, the other not. the one with future acts as a library for the one that does not have them. The annotations of a subclass declared against a base class that comes from the library are also not showing up if the base has a custom metaclasslib.py
then make app.py:
annotations for "foo" are lost in 3.14.0b1
zzzeek commentedon May 8, 2025
also suffice to say this is a way worse issue for SQLAlchemy than the conditionals thing. we have no way to work around this
JelleZijlstra commentedon May 8, 2025
I think the workaround is to not use
from __future__ import annotations? Going to think about this some more; especially the fist report is unexpected. I want behavior underfrom __future__ import annotationsto remain unchanged from 3.13, but clearly I missed something.zzzeek commentedon May 8, 2025
That's not a workaround for us, this is the SQLAlchemy library. We use modern annotations so we need
from __future__ import annotationsin our code since we are supporting python 3.9 through current. our users might not use this. it's not an option for the annotations to just be lost when this directive is used in some of the source filesJelleZijlstra commentedon May 8, 2025
This change seems to fix your cases:
Unfortunately this issue brings us back to a state where
cls.__annotations__is unsafe, though no more so than before 3.14. (You'll see some interesting behavior in some of your examples if you use the__annotations__attribute directly instead ofget_annotations(); discussion in https://peps.python.org/pep-0749/#annotations-and-metaclasses .)I'll put up a PR with this change later after I've had some time to write tests. Thanks for the early feedback, and sorry I didn't test behavior with the future import enough.
zzzeek commentedon May 8, 2025
OK so I can just vendor that code right now to try to workaround? great
zzzeek commentedon May 8, 2025
that's ironically pretty much the code we used in python versions prior to 3.10
zzzeek commentedon May 8, 2025
this seems to be working though I am getting better results when I am using the exact version of
_get_dunder_annotationsthat's in 3.14.0a7:otherwise I'm getting errors on some other classes,
TypeError: '>' not supported between instances of 'builtin_function_or_method' and 'int'which it's not showing exactly what it's hitting, haven't dug into itzzzeek commentedon May 8, 2025
I found another issue with TypedDict, and it seems similar to these, but is different in that the
__annotations__dictionary is simply missing super "class" elements - this is unworkable because the superclasses as declared are not actually in__mro__. Going to make a separate issue, apologies if it's the same thingzzzeek commentedon May 8, 2025
that new report is at #133701
4 remaining items
JelleZijlstra commentedon May 9, 2025
Yeah we'll need to go back to the
_BASE_GET_ANNOTATIONShack. I'll work on it over the next few days, I want to add a similar set of test cases as in #133772 for different combinations of with/without the future.zzzeek commentedon May 9, 2025
OK the subsequent problem I'm having has to do with something related but out of the immediate scope here, I'm attempting to runtime set annotations on a class to something other than what they were, for the purposes of getting python's dataclass() function to interpret the class in a certain way, and if that class is in a hierarchy, the assignment of new annotations is not working. Admittedly setting annotations to something new is a little more of a hack, so something broke with that as of 3.14b1, so ill try to figure that out more.
JelleZijlstra commentedon May 9, 2025
That sounds like it should still work, if you can minimize the repro case please open a new issue. Thanks for all the testing!
zzzeek commentedon May 9, 2025
if you just assign to
__annotations__that seems to have changed in 3.14.0b1, it has no effect now. there seems to be a new attribute__annotations_cache__that's where the assignment goes but it's not usedJelleZijlstra commentedon May 9, 2025
Ugh that seems to be another
from __future__ import annotationsbug, I'll fix.zzzeek commentedon May 9, 2025
oh OK. I just put it in #133778 as you were typing this
pythongh-133684: Fix get_annotations() where PEP 563 is involved
gh-133684: Fix get_annotations() where PEP 563 is involved (#133795)
pythongh-133684: Fix get_annotations() where PEP 563 is involved (pyt…
[3.14] gh-133684: Fix get_annotations() where PEP 563 is involved (GH…
pythongh-133684: Fix get_annotations() where PEP 563 is involved (pyt…
pythongh-133684: Fix get_annotations() where PEP 563 is involved (pyt…
modernize annotationlib approach
pythongh-133684: Fix get_annotations() where PEP 563 is involved (pyt…