Description
Bug report
Bug description:
Any typing construct using typing._GenericAlias
(and not types.GenericAlias
) will convert the arguments to ForwardRef
instances at runtime:
from typing import List
class A[T]:
pass
List['F']
#> List[ForwardRef('F')]
A['F']
#> A[ForwardRef('F')]
Because _GenericAlias.__getitem__
calls are cached with the _tp_cache()
decorator, we end up with the same ForwardRef
instance used:
alias1 = List['F']
#> List[ForwardRef('F')]
alias2 = List['F']
#> List[ForwardRef('F')]
alias1.__args__[0] is alias2.__args__[0]
#> True
And this becomes an issue as ForwardRef._evaluate()
calls are also cached per instance. Consider the following setup:
# mod1.py
def func(a: List['Forward']): pass
Forward = int
typing.get_type_hints(func)
# {'a': List[int]}
# mod2.py
def func(a: List['Forward']): pass
Forward = str
typing.get_type_hints(func)
# {'a': List[int]}
Note that this is already an issue on Python < 3.14. However, the impact is somewhat limited as afaik this only happens with functions. The reason is that there is a really old cache invalidation mechanism seemingly introduced to avoid a leak issue in ForwardRef._evaluate
. This logic would force the evaluation of the forward reference (even if _evaluate
was already called) if the provided localns
is different from the globalns
(on L920
the evaluation logic goes on):
Lines 916 to 920 in dae5b16
Using typing.get_type_hints
, this condition is false when getting annotations of functions, because functions don't have locals and locals are set to globals if this is the case:
Lines 2441 to 2442 in 8f93dd8
However, the implementation of PEP 649 removed this check and the cached evaluated value is unconditionally used:
Lines 95 to 101 in 8f93dd8
While the described bug above is pretty uncommon as it only occurs with functions, it also happens in classes with 3.14:
# mod1.py
class A:
'a': List['Forward']
Forward = int
typing.get_type_hints(A)
# {'a': List[int]}
# mod2.py
class A:
'a': List['Forward']
Forward = str
typing.get_type_hints(func)
# {'a': List[int]}
And I believe this is going to cause some issues with existing code bases, especially the ones using runtime typing libraries. In Pydantic, we already had issues like this one, as we recently changed our global/local namespace logic (and as such, we had a report of the above issue with classes: cloudflare/cloudflare-python#116 (comment)).
While we might argue that string annotations are no longer needed in 3.14, it is still relevant for libraries which need to keep support for older Python versions.
One possible solution would be to have _tp_cache()
skip string arguments, so that we don't end up reusing the same ForwardRef
instances. Not sure how big the impact will be.
CPython versions tested on:
3.14, CPython main branch
Operating systems tested on:
No response