From be833e658aaf6703b0dd0c0dadb893d72cbe4c77 Mon Sep 17 00:00:00 2001 From: Shamil Date: Thu, 23 Apr 2026 05:31:58 +0300 Subject: [PATCH 1/7] gh-146553: Fix infinite loop in typing.get_type_hints() on circular __wrapped__ (#148595) --- Lib/test/test_typing.py | 18 ++++++++++++++++++ Lib/typing.py | 4 ++++ ...6-04-15-11-00-39.gh-issue-146553.VGOsoP.rst | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-15-11-00-39.gh-issue-146553.VGOsoP.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index bfae83fdaf6bc0..6c3d67fb6b7383 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6888,6 +6888,24 @@ def test_get_type_hints_wrapped_decoratored_func(self): self.assertEqual(gth(ForRefExample.func), expects) self.assertEqual(gth(ForRefExample.nested), expects) + def test_get_type_hints_wrapped_cycle_self(self): + # gh-146553: __wrapped__ self-reference must raise ValueError, + # not loop forever. + def f(x: int) -> str: ... + f.__wrapped__ = f + with self.assertRaisesRegex(ValueError, 'wrapper loop'): + get_type_hints(f) + + def test_get_type_hints_wrapped_cycle_mutual(self): + # gh-146553: mutual __wrapped__ cycle (a -> b -> a) must raise + # ValueError, not loop forever. + def a(): ... + def b(): ... + a.__wrapped__ = b + b.__wrapped__ = a + with self.assertRaisesRegex(ValueError, 'wrapper loop'): + get_type_hints(a) + def test_get_type_hints_annotated(self): def foobar(x: List['X']): ... X = Annotated[int, (1, 10)] diff --git a/Lib/typing.py b/Lib/typing.py index 3e7661dd2f877c..46e7122b6c91c5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2486,8 +2486,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, else: nsobj = obj # Find globalns for the unwrapped object. + seen = {id(nsobj)} while hasattr(nsobj, '__wrapped__'): nsobj = nsobj.__wrapped__ + if id(nsobj) in seen: + raise ValueError(f'wrapper loop when unwrapping {obj!r}') + seen.add(id(nsobj)) globalns = getattr(nsobj, '__globals__', {}) if localns is None: localns = globalns diff --git a/Misc/NEWS.d/next/Library/2026-04-15-11-00-39.gh-issue-146553.VGOsoP.rst b/Misc/NEWS.d/next/Library/2026-04-15-11-00-39.gh-issue-146553.VGOsoP.rst new file mode 100644 index 00000000000000..44216318d474a9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-15-11-00-39.gh-issue-146553.VGOsoP.rst @@ -0,0 +1,2 @@ +Fix infinite loop in :func:`typing.get_type_hints` when ``__wrapped__`` +forms a cycle. Patch by Shamil Abdulaev. From 8e43f3d1177f22c95f5fc66349a3b748a36470c9 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 23 Apr 2026 04:39:08 +0200 Subject: [PATCH 2/7] gh-145056: Add support for frozendict in dataclass asdict and astuple (#145125) --- Doc/library/dataclasses.rst | 8 ++++---- Lib/dataclasses.py | 10 ++++++---- Lib/test/test_dataclasses/__init__.py | 20 ++++++++++++++++--- ...-02-22-19-36-00.gh-issue-145056.TH8nX4.rst | 1 + 4 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index fd8e0c0bea1cb1..0bce3e5b762b8b 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -371,8 +371,8 @@ Module contents Converts the dataclass *obj* to a dict (by using the factory function *dict_factory*). Each dataclass is converted to a dict of its fields, as ``name: value`` pairs. dataclasses, dicts, - lists, and tuples are recursed into. Other objects are copied with - :func:`copy.deepcopy`. + frozendicts, lists, and tuples are recursed into. Other objects are copied + with :func:`copy.deepcopy`. Example of using :func:`!asdict` on nested dataclasses:: @@ -402,8 +402,8 @@ Module contents Converts the dataclass *obj* to a tuple (by using the factory function *tuple_factory*). Each dataclass is converted - to a tuple of its field values. dataclasses, dicts, lists, and - tuples are recursed into. Other objects are copied with + to a tuple of its field values. dataclasses, dicts, frozendicts, lists, + and tuples are recursed into. Other objects are copied with :func:`copy.deepcopy`. Continuing from the previous example:: diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 0c7e01cb16b192..9d5bed6b96fc49 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1496,7 +1496,8 @@ class C: If given, 'dict_factory' will be used instead of built-in dict. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): raise TypeError("asdict() should be called on dataclass instances") @@ -1552,7 +1553,7 @@ def _asdict_inner(obj, dict_factory): return obj_type(*[_asdict_inner(v, dict_factory) for v in obj]) else: return obj_type(_asdict_inner(v, dict_factory) for v in obj) - elif issubclass(obj_type, dict): + elif issubclass(obj_type, (dict, frozendict)): if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from # dict as it requires the default_factory as its first arg. @@ -1587,7 +1588,8 @@ class C: If given, 'tuple_factory' will be used instead of built-in tuple. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): @@ -1616,7 +1618,7 @@ def _astuple_inner(obj, tuple_factory): # generator (which is not true for namedtuples, handled # above). return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) - elif isinstance(obj, dict): + elif isinstance(obj, (dict, frozendict)): obj_type = type(obj) if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index b44b1da0336d09..e0cfe3df3e6357 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1693,17 +1693,24 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(asdict(gl), {'id': 0, 'users': [{'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2}]}) self.assertEqual(asdict(gt), {'id': 0, 'users': ({'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2})}) - self.assertEqual(asdict(gd), {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, - 'second': {'name': 'Bob', 'id': 2}}}) + expected_dict = {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, + 'second': {'name': 'Bob', 'id': 2}}} + self.assertEqual(asdict(gd), expected_dict) + self.assertEqual(asdict(gfd), expected_dict) def test_helper_asdict_builtin_object_containers(self): @dataclass @@ -1884,14 +1891,21 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(astuple(gl), (0, [('Alice', 1), ('Bob', 2)])) self.assertEqual(astuple(gt), (0, (('Alice', 1), ('Bob', 2)))) - self.assertEqual(astuple(gd), (0, {'first': ('Alice', 1), 'second': ('Bob', 2)})) + d = {'first': ('Alice', 1), 'second': ('Bob', 2)} + self.assertEqual(astuple(gd), (0, d)) + self.assertEqual(astuple(gfd), (0, frozendict(d))) def test_helper_astuple_builtin_object_containers(self): @dataclass diff --git a/Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst b/Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst new file mode 100644 index 00000000000000..45be0109677cd1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst @@ -0,0 +1 @@ +Add support for :class:`frozendict` in :meth:`dataclasses.asdict` and :meth:`dataclasses.astuple`. From bd7352d8071dc00531f2c527977602729f2d3ec6 Mon Sep 17 00:00:00 2001 From: Vikash Kumar <163628932+Vikash-Kumar-23@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:10:10 +0530 Subject: [PATCH 3/7] gh-145194: Fix typing in re tokenizer example (#145198) --- Doc/library/re.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/re.rst b/Doc/library/re.rst index 6ed285c4b11213..a46fd42458158c 100644 --- a/Doc/library/re.rst +++ b/Doc/library/re.rst @@ -1953,7 +1953,7 @@ successive matches:: class Token(NamedTuple): type: str - value: str + value: int | float | str line: int column: int From 75ff1afcb6a1bb2b3d54899e9b222a61798fa491 Mon Sep 17 00:00:00 2001 From: John Seong <39040639+sandole@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:46:04 +0800 Subject: [PATCH 4/7] gh-142965: Fix Concatenate documentation to reflect valid use cases (#143316) The documentation previously stated that Concatenate is only valid when used as the first argument to Callable, but according to PEP 612, it can also be used when instantiating user-defined generic classes with ParamSpec parameters. --- Doc/library/typing.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 9bc0a3caeee8bf..04acf2c16d15d2 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1174,7 +1174,8 @@ These can be used as types in annotations. They all support subscription using or transforms parameters of another callable. Usage is in the form ``Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable]``. ``Concatenate`` - is currently only valid when used as the first argument to a :ref:`Callable `. + is valid when used in :ref:`Callable ` type hints + and when instantiating user-defined generic classes with :class:`ParamSpec` parameters. The last parameter to ``Concatenate`` must be a :class:`ParamSpec` or ellipsis (``...``). From 8bf99ae3a9f12d105a70d6fda93dddde4adeee8f Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:50:15 +0200 Subject: [PATCH 5/7] gh-119180: Document the `format` parameter in `typing.get_type_hints()` (#143758) Do not mention `__annotations__` dictionaries, as this is slightly outdated since 3.14. Rewrite the note about possible exceptions for clarity. Also do not mention imported type aliases, as since 3.12 aliases with the `type` statement do not suffer from this limitation anymore. --- Doc/library/typing.rst | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 04acf2c16d15d2..1957cadcbb1592 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -3453,13 +3453,13 @@ Functions and decorators Introspection helpers --------------------- -.. function:: get_type_hints(obj, globalns=None, localns=None, include_extras=False) +.. function:: get_type_hints(obj, globalns=None, localns=None, include_extras=False, *, format=Format.VALUE) Return a dictionary containing type hints for a function, method, module, class object, or other callable object. - This is often the same as ``obj.__annotations__``, but this function makes - the following changes to the annotations dictionary: + This is often the same as :func:`annotationlib.get_annotations`, but this + function makes the following changes to the annotations dictionary: * Forward references encoded as string literals or :class:`ForwardRef` objects are handled by evaluating them in *globalns*, *localns*, and @@ -3473,17 +3473,15 @@ Introspection helpers annotations from ``C``'s base classes with those on ``C`` directly. This is done by traversing :attr:`C.__mro__ ` and iteratively combining - ``__annotations__`` dictionaries. Annotations on classes appearing - earlier in the :term:`method resolution order` always take precedence over - annotations on classes appearing later in the method resolution order. + :term:`annotations ` of each base class. Annotations + on classes appearing earlier in the :term:`method resolution order` always + take precedence over annotations on classes appearing later in the method + resolution order. * The function recursively replaces all occurrences of ``Annotated[T, ...]``, ``Required[T]``, ``NotRequired[T]``, and ``ReadOnly[T]`` with ``T``, unless *include_extras* is set to ``True`` (see :class:`Annotated` for more information). - See also :func:`annotationlib.get_annotations`, a lower-level function that - returns annotations more directly. - .. caution:: This function may execute arbitrary code contained in annotations. @@ -3491,11 +3489,12 @@ Introspection helpers .. note:: - If any forward references in the annotations of *obj* are not resolvable - or are not valid Python code, this function will raise an exception - such as :exc:`NameError`. For example, this can happen with imported - :ref:`type aliases ` that include forward references, - or with names imported under :data:`if TYPE_CHECKING `. + If :attr:`Format.VALUE ` is used and any + forward references in the annotations of *obj* are not resolvable, a + :exc:`NameError` exception is raised. For example, this can happen + with names imported under :data:`if TYPE_CHECKING `. + More generally, any kind of exception can be raised if an annotation + contains invalid Python code. .. note:: @@ -3513,6 +3512,10 @@ Introspection helpers if a default value equal to ``None`` was set. Now the annotation is returned unchanged. + .. versionchanged:: 3.14 + Added the ``format`` parameter. See the documentation on + :func:`annotationlib.get_annotations` for more information. + .. versionchanged:: 3.14 Calling :func:`get_type_hints` on instances is no longer supported. Some instances were accepted in earlier versions as an undocumented From fbc7676df6256071682f4179818b74ba29f162cd Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 22 Apr 2026 22:06:56 -0500 Subject: [PATCH 6/7] Speed up counting in statistics.fmean() (gh-148875) --- Lib/statistics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/statistics.py b/Lib/statistics.py index e635b99f958e44..32fcf2313a815a 100644 --- a/Lib/statistics.py +++ b/Lib/statistics.py @@ -136,7 +136,7 @@ from fractions import Fraction from decimal import Decimal -from itertools import count, groupby, repeat +from itertools import compress, count, groupby, repeat from bisect import bisect_left, bisect_right from math import hypot, sqrt, fabs, exp, erfc, tau, log, fsum, sumprod from math import isfinite, isinf, pi, cos, sin, tan, cosh, asin, atan, acos @@ -195,9 +195,9 @@ def fmean(data, weights=None): n = len(data) except TypeError: # Handle iterators that do not define __len__(). - counter = count() - total = fsum(map(itemgetter(0), zip(data, counter))) - n = next(counter) + counter = count(1) + total = fsum(compress(data, counter)) + n = next(counter) - 1 else: total = fsum(data) From 3b9397988d1f83740e7d73d17d56767976a583b4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 22 Apr 2026 22:00:35 -0600 Subject: [PATCH 7/7] gh-148892: Drop mention of deprecated cibuildwheel option (#148893) --- Doc/howto/free-threading-extensions.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Doc/howto/free-threading-extensions.rst b/Doc/howto/free-threading-extensions.rst index 2f089a3d89680a..b21ed1c8f37be1 100644 --- a/Doc/howto/free-threading-extensions.rst +++ b/Doc/howto/free-threading-extensions.rst @@ -416,11 +416,9 @@ C API extensions need to be built specifically for the free-threaded build. The wheels, shared libraries, and binaries are indicated by a ``t`` suffix. * `pypa/manylinux `_ supports the - free-threaded build, with the ``t`` suffix, such as ``python3.13t``. -* `pypa/cibuildwheel `_ supports the - free-threaded build on Python 3.13 and 3.14. On Python 3.14, free-threaded - wheels will be built by default. On Python 3.13, you will need to set - `CIBW_ENABLE to cpython-freethreading `_. + free-threaded build, with the ``t`` suffix, such as ``python3.14t``. +* `pypa/cibuildwheel `_ supports + building wheels for the free-threaded build of Python 3.14 and newer. Limited C API and Stable ABI ............................