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
[I believe this is fundamentally a dataclass version of https://bugs.python.org/issue41249]
When using from __future__ import annotations, calling get_type_hints on the constructor of a dataclass B which inherits from a dataclass A defined in another module will error if dataclass A has type hints which are not imported in the module where dataclass B is defined.
This is best shown by example, if you have foo.py:
from __future__ import annotations
import collections
import dataclasses
@dataclasses.dataclass
class A:
x: collections.OrderedDict
and then in bar.py:
from __future__ import annotations
import foo
import dataclasses
import typing
@dataclasses.dataclass
class B(foo.A):
pass
typing.get_type_hints(B)
the final line will raise "NameError: name 'collections' is not defined".
This code will not error if you do either of the following:
add import collections to bar.py.
remove the future annotations import from both files.
I am not confident enough on the internals of dataclass to suggest a fix, but potentially a similar approach to that which solved the TypedDict equivalent https://bugs.python.org/issue41249 would work?
I can't reproduce this on Python 3.8.3, 3.9.6, 3.10.0 or 3.11.0a1+. Which versions of Python have you tried this on? (I'm able to reproduce the typing.TypedDict bug on Python 3.8, since the fix was only backported to 3.9, but not this.)
slebedev commented on Oct 19, 2021
slebedevmannequin
Mannequin
More actions
I think the example has a minor typo.
The crash is reproducible on 3.9 if the last line in bar.py is
Ah yes, I completely apologize about that typo, the __init__ is definitely needed (serves me right for trying to clean up a repo before posting it).
One other comment to make, perhaps obvious; manually passing a namespace to get_type_hints' globalns which contains collections does indeed solve the problem (in case anyone else sees this and is blocked). Of course determining the right values to pass is in general difficult for a user (and shouldn't be necessary).
(I'm not proposing adding two new files to the cpython test suite -- just put the tests in new files so that I could isolate the new tests from the rest of the test suite and understand the problem better.)
If you try running my test script with the from __future__ import annotations line at the top commented out, however, the error is the same. (from __future__ import annotations is obviously still there in the module that's being imported by the test script.)
slebedev commented on Oct 22, 2021
slebedevmannequin
Mannequin
More actions
Is it worth also addressing the case where a @dataclass/typing.TypeDict class is defined within a function?
from __future__ import annotations
import typing
from dataclasses import dataclass
def make_A():
import collections
@dataclass
class A:
x: collections.defaultdict
return A
A = make_A()
@dataclass
class B(A):
y: int
# NameError: name 'collections' is not defined
print(typing.get_type_hints(B.__init__))
@sergei, I believe that's a much larger issue, and one that's quite difficult to solve. It's one of the principal reasons why from __future__ import annotations behaviour wasn't made the default in Python 3.10, as was originally the plan. (This behaviour doesn't play nicely with libraries such as pydantic that analyse annotations at runtime.) I think it's probably beyond the scope of this BPO issue :)
I've tried python3.14rc1 with annotationlib.get_annotations. It is still broken in the same way as it is with get_type_hints
Traceback (most recent call last):
File "/src/tmp/dtc/child.py", line 21, in <module>
print(annotationlib.get_annotations(Child.__init__, eval_str=True))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/annotationlib.py", line 998, in get_annotations
key: value if not isinstance(value, str) else eval(value, globals, locals)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
NameError: name 'LocalDep' is not defined
You are not showing a full code sample so I can't tell, but the problem may be that you're using from __future__ import annotations. The behavior under the future import is largely unchanged in Python 3.14. If you want good introspection, don't use from __future__ import annotations.
I don't think so; its globals arguments defaults to None, not the calling module's globals(). Indeed, typing.evaluate_forward_ref(x) succeeds for your example too.
I realize this might cause problems though if you call get_type_hints() and need to pass globals=globals() for some ForwardRefs in there but not others. Not sure when that would happen though; I'd love self-contained examples where the current behavior doesn't work well.
Playing with order of A and D, if the annotation is string or not, I got different behavior, but it should not be different.
To fix this we should have a type hint holding module, but there isn't anything exceptForwardRef
In cases ForwardRef has owner or module set ( ForwardRef('A', is_class=True, owner=<class 'a.D'>)) it is resolved by get_type_hints ignoring information about it's origin.
string annotations in class are not converted to ForwardRef and __annotations__ strores them as is
TypeAliasType doesn't expect module argument and is immutable so cannot be used as a lazy proxy to another module
dataclasses copy annotations of all fields (including parent ones) to __init__
So, the question is:
How can I create an annotation (in runtume) which is lazy and references to the same type even if used or resolved in another place?
As far as I can tell, things work correctly when creating a ForwardRef with an appropriate owner= and calling .evaluate(). Things also work correctly if you don't use string annotations.
What doesn't work well is get_type_hints(), because it's prone to passing in the wrong globals when it evaluates ForwardRefs. Unfortunately, that function has a lot of legacy behavior and is difficult to change. In user code, I'd recommend performing a simplified version of get_type_hints() directly, i.e., calling annotationlib.get_annotations(), converting any raw strings into ForwardRefs with the appropriate owner, and then calling ForwardRef.evaluate() as appropriate.
Dataclasses could do better at preserving the owner of field types in the case of inheritance. I think this patch would be an improvement:
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 53b3b54cfb3..e3d25fb0840 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -789,7 +789,10 @@ def _get_field(cls, a_name, a_type, default_kw_only):
# Only at this point do we know the name and the type. Set them.
f.name = a_name
- f.type = a_type
+ if isinstance(a_type, str):
+ f.type = annotationlib.ForwardRef(a_type, owner=cls)
+ else:
+ f.type = a_type
# Assume it's a normal field until proven otherwise. We're next
# going to decide if it's a ClassVar or InitVar, everything else
Ok, setting a ForwardRef with owner right on field type sounds better, but anyway it does nothing if globals/locals are passed. Looks like the encapuslation here is broken.
I still have no idea about the idea behind the module/owner. From my perspecitve any ForwardRef should be bounded to a scope, so it can still access the same value in that scope. If ForwardRef is created within a specific module, it is expected to be solved using objects in that module. It is not a matter of solver but an origin where it is created.
Talking about this, I can say that I do not see much difference between TypeAliasType and ForwardRef, but you cannot create TypeAliasType instance providing another module so it cannot be used here.
If you have any links related the design of ForwardRef attributes, please share
Activity
aidanbclark commentedon Oct 19, 2021
[I believe this is fundamentally a dataclass version of https://bugs.python.org/issue41249]
When using
from __future__ import annotations, calling get_type_hints on the constructor of a dataclass B which inherits from a dataclass A defined in another module will error if dataclass A has type hints which are not imported in the module where dataclass B is defined.This is best shown by example, if you have foo.py:
and then in bar.py:
the final line will raise "NameError: name 'collections' is not defined".
This code will not error if you do either of the following:
import collectionsto bar.py.I am not confident enough on the internals of dataclass to suggest a fix, but potentially a similar approach to that which solved the TypedDict equivalent https://bugs.python.org/issue41249 would work?
AlexWaygood commentedon Oct 19, 2021
I can't reproduce this on Python 3.8.3, 3.9.6, 3.10.0 or 3.11.0a1+. Which versions of Python have you tried this on? (I'm able to reproduce the
typing.TypedDictbug on Python 3.8, since the fix was only backported to 3.9, but not this.)slebedev commentedon Oct 19, 2021
I think the example has a minor typo.
The crash is reproducible on 3.9 if the last line in bar.py is
instead of
AlexWaygood commentedon Oct 19, 2021
Thanks @sergei. With that alteration, I have reproduced this on Python 3.9, 3.10 and 3.11.
aidanbclark commentedon Oct 19, 2021
Ah yes, I completely apologize about that typo, the __init__ is definitely needed (serves me right for trying to clean up a repo before posting it).
One other comment to make, perhaps obvious; manually passing a namespace to get_type_hints'
globalnswhich containscollectionsdoes indeed solve the problem (in case anyone else sees this and is blocked). Of course determining the right values to pass is in general difficult for a user (and shouldn't be necessary).sobolevn commentedon Oct 22, 2021
I had some time to debug this. It happens because we treat classes and functions differently.
The difference between
get_type_hints(B)andget_type_hints(B.__init__)is that we use differentglobalcontexts there:Bwe use__mro__entries and extractglobalsandlocalsfrom there:cpython/Lib/typing.py
Lines 1792 to 1796 in 86dfb55
B.__init__we use simplier logiccpython/Lib/typing.py
Lines 1822 to 1828 in 86dfb55
__mro__and super contextsFunny thing, this problem goes away if we remove
@dataclassdecorator and convert our examples into regular classes:and
I am going to try to solve this with something really simple (if no one else is working on it).
AlexWaygood commentedon Oct 22, 2021
@nikita, I had a go at writing some more rigorous tests regarding this issue. I found the same thing you did -- the issue seems:
My tests are in these two files on my cpython fork:
(I'm not proposing adding two new files to the cpython test suite -- just put the tests in new files so that I could isolate the new tests from the rest of the test suite and understand the problem better.)
AlexWaygood commentedon Oct 22, 2021
If you try running my test script with the
from __future__ import annotationsline at the top commented out, however, the error is the same. (from __future__ import annotationsis obviously still there in the module that's being imported by the test script.)slebedev commentedon Oct 22, 2021
Is it worth also addressing the case where a @dataclass/typing.TypeDict class is defined within a function?
AlexWaygood commentedon Oct 22, 2021
@sergei, I believe that's a much larger issue, and one that's quite difficult to solve. It's one of the principal reasons why
from __future__ import annotationsbehaviour wasn't made the default in Python 3.10, as was originally the plan. (This behaviour doesn't play nicely with libraries such as pydantic that analyse annotations at runtime.) I think it's probably beyond the scope of this BPO issue :)https://mail.python.org/archives/list/python-dev@python.org/thread/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/
11 remaining items
Tishka17 commentedon Jul 28, 2025
I've tried python3.14rc1 with
annotationlib.get_annotations. It is still broken in the same way as it is withget_type_hintsJelleZijlstra commentedon Jul 28, 2025
You are not showing a full code sample so I can't tell, but the problem may be that you're using
from __future__ import annotations. The behavior under the future import is largely unchanged in Python 3.14. If you want good introspection, don't usefrom __future__ import annotations.pythongh-89687: fix get_type_hints with dataclasses __init__ generation
pythongh-89687: fix get_type_hints with dataclasses __init__ generation
pythongh-89687: fix get_type_hints with dataclasses __init__ generation
Tishka17 commentedon Jul 28, 2025
The current problem is:
a.py:
b.py:
This raises an assertion error
JelleZijlstra commentedon Jul 28, 2025
If you pass
globals(), it uses those globals in preference to theownerparameter. This could perhaps be made more explicit in https://docs.python.org/3.14/library/annotationlib.html#annotationlib.ForwardRef.evaluate .Tishka17 commentedon Jul 28, 2025
It should not, I believe, because there is no other way to make forward ref bounded to module
JelleZijlstra commentedon Jul 28, 2025
It works if you just call
x.evaluate()in your example without passing explicitglobals().I'm not sure why you'd expect passing a different module's globals would help evaluate the ForwardRef.
Tishka17 commentedon Jul 28, 2025
Because it's what's happening inside
get_type_hintsJelleZijlstra commentedon Jul 28, 2025
I don't think so; its
globalsarguments defaults to None, not the calling module'sglobals(). Indeed,typing.evaluate_forward_ref(x)succeeds for your example too.I realize this might cause problems though if you call
get_type_hints()and need to passglobals=globals()for some ForwardRefs in there but not others. Not sure when that would happen though; I'd love self-contained examples where the current behavior doesn't work well.Tishka17 commentedon Jul 28, 2025
a.py:
b.py:
Playing with order of
AandD, if the annotation is string or not, I got different behavior, but it should not be different.To fix this we should have a type hint holding module, but there isn't anything exceptForwardRef
Tishka17 commentedon Jul 29, 2025
What I see now:
ownerormoduleset ( ForwardRef('A', is_class=True, owner=<class 'a.D'>)) it is resolved byget_type_hintsignoring information about it's origin.ForwardRefand__annotations__strores them as isTypeAliasTypedoesn't expect module argument and is immutable so cannot be used as a lazy proxy to another module__init__So, the question is:
How can I create an annotation (in runtume) which is lazy and references to the same type even if used or resolved in another place?
JelleZijlstra commentedon Jul 30, 2025
As far as I can tell, things work correctly when creating a
ForwardRefwith an appropriateowner=and calling.evaluate(). Things also work correctly if you don't use string annotations.What doesn't work well is
get_type_hints(), because it's prone to passing in the wrong globals when it evaluates ForwardRefs. Unfortunately, that function has a lot of legacy behavior and is difficult to change. In user code, I'd recommend performing a simplified version ofget_type_hints()directly, i.e., callingannotationlib.get_annotations(), converting any raw strings into ForwardRefs with the appropriateowner, and then callingForwardRef.evaluate()as appropriate.Dataclasses could do better at preserving the owner of field types in the case of inheritance. I think this patch would be an improvement:
Tishka17 commentedon Jul 31, 2025
Ok, setting a
ForwardRefwithownerright on field type sounds better, but anyway it does nothing ifglobals/localsare passed. Looks like the encapuslation here is broken.I still have no idea about the idea behind the module/owner. From my perspecitve any
ForwardRefshould be bounded to a scope, so it can still access the same value in that scope. IfForwardRefis created within a specific module, it is expected to be solved using objects in that module. It is not a matter of solver but an origin where it is created.Talking about this, I can say that I do not see much difference between
TypeAliasTypeandForwardRef, but you cannot createTypeAliasTypeinstance providing another module so it cannot be used here.If you have any links related the design of
ForwardRefattributes, please share