Title: PEP 649 and `ForwardRef` caching · Issue #128593 · python/cpython · GitHub
Open Graph Title: PEP 649 and `ForwardRef` caching · Issue #128593 · python/cpython
X Title: PEP 649 and `ForwardRef` caching · Issue #128593 · python/cpython
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...
Open Graph 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...
X 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...
Opengraph URL: https://github.com/python/cpython/issues/128593
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"PEP 649 and `ForwardRef` caching","articleBody":"# Bug report\r\n\r\n### Bug description:\r\n\r\nAny typing construct using `typing._GenericAlias` (and not `types.GenericAlias`) will convert the arguments to `ForwardRef` instances at runtime:\r\n\r\n```python\r\nfrom typing import List\r\n\r\nclass A[T]:\r\n pass\r\n\r\nList['F']\r\n#\u003e List[ForwardRef('F')]\r\nA['F']\r\n#\u003e A[ForwardRef('F')]\r\n```\r\n\r\nBecause `_GenericAlias.__getitem__` calls are cached with the `_tp_cache()` decorator, we end up with the same `ForwardRef` instance used:\r\n\r\n```python\r\nalias1 = List['F']\r\n#\u003e List[ForwardRef('F')]\r\nalias2 = List['F']\r\n#\u003e List[ForwardRef('F')]\r\n\r\nalias1.__args__[0] is alias2.__args__[0]\r\n#\u003e True\r\n```\r\n\r\nAnd this becomes an issue as `ForwardRef._evaluate()` calls are also cached per instance. Consider the following setup:\r\n\r\n```python\r\n# mod1.py\r\n\r\ndef func(a: List['Forward']): pass\r\n\r\nForward = int\r\n\r\ntyping.get_type_hints(func)\r\n# {'a': List[int]}\r\n\r\n# mod2.py\r\n\r\ndef func(a: List['Forward']): pass\r\n\r\nForward = str\r\n\r\ntyping.get_type_hints(func)\r\n# {'a': List[int]}\r\n```\r\n\r\nNote that this is already an issue on Python \u003c 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](https://github.com/python/typing/pull/328) 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):\r\n\r\nhttps://github.com/python/cpython/blob/dae5b16bb3b51702d3506ef21550b220359ff971/Lib/typing.py#L916-L920\r\n\r\nUsing `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:\r\n\r\nhttps://github.com/python/cpython/blob/8f93dd8a8f237b277abad20d566df90c5cbd7f1e/Lib/typing.py#L2441-L2442\r\n\r\n---\r\n\r\nHowever, the implementation of PEP 649 removed this check and the cached evaluated value is unconditionally used:\r\n\r\nhttps://github.com/python/cpython/blob/8f93dd8a8f237b277abad20d566df90c5cbd7f1e/Lib/annotationlib.py#L95-L101\r\n\r\nWhile the described bug above is pretty uncommon as it only occurs with functions, it also happens in classes with 3.14:\r\n\r\n```python\r\n# mod1.py\r\n\r\nclass A:\r\n 'a': List['Forward']\r\n\r\nForward = int\r\n\r\ntyping.get_type_hints(A)\r\n# {'a': List[int]}\r\n\r\n# mod2.py\r\n\r\nclass A:\r\n 'a': List['Forward']\r\n\r\nForward = str\r\n\r\ntyping.get_type_hints(func)\r\n# {'a': List[int]}\r\n```\r\n\r\nAnd 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: https://github.com/cloudflare/cloudflare-python/discussions/116#discussioncomment-11372711).\r\n\r\nWhile 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.\r\n\r\nOne 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. \r\n\r\n### CPython versions tested on:\r\n\r\n3.14, CPython main branch\r\n\r\n### Operating systems tested on:\r\n\r\n_No response_","author":{"url":"https://github.com/Viicos","@type":"Person","name":"Viicos"},"datePublished":"2025-01-07T18:58:56.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":2},"url":"https://github.com/128593/cpython/issues/128593"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:f651b12b-c354-f8e4-d503-6278f7a0abfa |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | D870:37464F:C98686:11BA59F:696A216F |
| html-safe-nonce | 368c6caafbc894f7d08551c7c3a967045424093593675f69b3f4628de19838e5 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEODcwOjM3NDY0RjpDOTg2ODY6MTFCQTU5Rjo2OTZBMjE2RiIsInZpc2l0b3JfaWQiOiI1NjY3NjQ0NTE0OTI2OTkzNzc1IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 8e7e661e2978cb638728aef5f4c711024d2392c52ecea95c931b951d397b2c48 |
| hovercard-subject-tag | issue:2773532411 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/python/cpython/128593/issue_layout |
| twitter:image | https://opengraph.githubassets.com/90b3b252168109759ba7d1bb53d8eac8eba6b591cab1a398c5f22af4385ee458/python/cpython/issues/128593 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/90b3b252168109759ba7d1bb53d8eac8eba6b591cab1a398c5f22af4385ee458/python/cpython/issues/128593 |
| og:image:alt | 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... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | Viicos |
| hostname | github.com |
| expected-hostname | github.com |
| None | 014f3d193f36b7d393f88ca22d06fbacd370800b40a547c1ea67291e02dc8ea3 |
| turbo-cache-control | no-preview |
| go-import | github.com/python/cpython git https://github.com/python/cpython.git |
| octolytics-dimension-user_id | 1525981 |
| octolytics-dimension-user_login | python |
| octolytics-dimension-repository_id | 81598961 |
| octolytics-dimension-repository_nwo | python/cpython |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 81598961 |
| octolytics-dimension-repository_network_root_nwo | python/cpython |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | d515f6f09fa57a93bf90355cb894eb84ca4f458f |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width