From 4848725a07f1185a334918e7404fa6a0d36f2486 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:04:06 +1000 Subject: [PATCH 1/2] add bound/variance parameters to `TypeVarTuple` --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 52 +++++++++++++++++++++++++++++++++-- src/typing_extensions.py | 50 +++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd2db5f..da8f070d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Python 3.9. The `typing` implementation has always raised an error, and the `typing_extensions` implementation has raised an error on Python 3.10+ since `typing_extensions` v4.6.0. Patch by Brian Schubert. +- Add `bound` and variance parameters to `TypeVarTuple`. # Release 4.15.0 (August 25, 2025) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4149eebe..1cfdfda5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1762,7 +1762,7 @@ def test_annotation_and_optional_default(self): annotation : annotation, Optional[int] : Optional[int], Optional[List[str]] : Optional[List[str]], - Optional[annotation] : Optional[annotation], + Optional[annotation] : Optional[annotation], Union[str, None, str] : Optional[str], Unpack[Tuple[int, None]]: Unpack[Tuple[int, None]], } @@ -1780,6 +1780,8 @@ def test_annotation_and_optional_default(self): Union[str, "Union[None, StrAlias]"]: Optional[str], Union["annotation", T_default] : Union[annotation, T_default], Annotated["annotation", "nested"] : Annotated[Union[int, None], "data", "nested"], + # Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485 + Unpack[Ts] : Unpack[Ts], } # Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485 if TYPING_3_15_0: @@ -6610,7 +6612,10 @@ def test_basic_plain(self): @skipIf(TYPING_3_15_0, "repr changed in 3.15") def test_repr(self): Ts = TypeVarTuple('Ts') - self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[Ts]') + if not hasattr(typing, 'TypeVarTuple') or sys.version_info >= (3, 15): + self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[~Ts]') + else: + self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[Ts]') @skipUnless(TYPING_3_15_0, "repr changed in 3.15") def test_repr_py315(self): @@ -6814,7 +6819,44 @@ def test_basic_plain(self): @skipIf(TYPING_3_15_0, "repr changed in 3.15") def test_repr(self): Ts = TypeVarTuple('Ts') - self.assertEqual(repr(Ts), 'Ts') + Ts_co = TypeVarTuple('Ts_co', covariant=True) + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + Ts_infer = TypeVarTuple('Ts_infer', infer_variance=True) + Ts_2 = TypeVarTuple('Ts_2') + if not hasattr(typing, 'TypeVarTuple') or sys.version_info >= (3, 15): + self.assertEqual(repr(Ts), '~Ts') + self.assertEqual(repr(Ts_2), '~Ts_2') + + self.assertEqual(repr(Ts_co), '+Ts_co') + self.assertEqual(repr(Ts_contra), '-Ts_contra') + self.assertEqual(repr(Ts_infer), 'Ts_infer') + else: + # On other versions we use typing.TypeVarTuple, but it is not aware of + # variance. Not worth creating our own version of TypeVarTuple + # for this. + self.assertEqual(repr(Ts), 'Ts') + self.assertEqual(repr(Ts_2), 'Ts_2') + + self.assertEqual(repr(Ts_co), 'Ts_co') + self.assertEqual(repr(Ts_contra), 'Ts_contra') + self.assertEqual(repr(Ts_infer), 'Ts_infer') + + def test_variance(self): + Ts_co = TypeVarTuple('Ts_co', covariant=True) + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + Ts_infer = TypeVarTuple('Ts_infer', infer_variance=True) + + self.assertIs(Ts_co.__covariant__, True) + self.assertIs(Ts_co.__contravariant__, False) + self.assertIs(Ts_co.__infer_variance__, False) + + self.assertIs(Ts_contra.__covariant__, False) + self.assertIs(Ts_contra.__contravariant__, True) + self.assertIs(Ts_contra.__infer_variance__, False) + + self.assertIs(Ts_infer.__covariant__, False) + self.assertIs(Ts_infer.__contravariant__, False) + self.assertIs(Ts_infer.__infer_variance__, True) @skipUnless(TYPING_3_15_0, "repr changed in 3.15") def test_repr_py315(self): @@ -7145,6 +7187,10 @@ def test_typing_extensions_defers_when_possible(self): exclude |= { 'TypeAliasType', 'Protocol' } + if sys.version_info < (3, 15): + exclude |= { + 'TypeVarTuple' + } if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 64b2676b..395475d0 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1750,7 +1750,10 @@ def TypeAlias(self, parameters): def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault - type_param.__default__ = default + if default is NoDefault: + type_param.__default__ = default + else: + type_param.__default__ = typing._type_check(default, "Default must be a type.") def _set_module(typevarlike): @@ -1903,7 +1906,7 @@ def __new__(cls, name, *, bound=None, paramspec = typing.ParamSpec(name, bound=bound, covariant=covariant, contravariant=contravariant) - paramspec.__infer_variance__ = infer_variance + paramspec.__infer_variance__ = bool(infer_variance) _set_default(paramspec, default) _set_module(paramspec) @@ -2650,20 +2653,33 @@ def _unpack_args(*args): return newargs -if _PEP_696_IMPLEMENTED: +if sys.version_info >= (3, 15): from typing import TypeVarTuple elif hasattr(typing, "TypeVarTuple"): # 3.11+ - # Add default parameter - PEP 696 + # Add default parameter - PEP 696 and bound/variance parameters class TypeVarTuple(metaclass=_TypeVarLikeMeta): """Type variable tuple.""" _backported_typevarlike = typing.TypeVarTuple - def __new__(cls, name, *, default=NoDefault): - tvt = typing.TypeVarTuple(name) - _set_default(tvt, default) + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + + if _PEP_696_IMPLEMENTED: + # can pass default argument + tvt = typing.TypeVarTuple(name, default=default) + else: + tvt = typing.TypeVarTuple(name) + _set_default(tvt, default) + + tvt.__bound__ = typing._type_check(bound, "Bound must be a type.") + tvt.__covariant__ = bool(covariant) + tvt.__contravariant__ = bool(contravariant) + tvt.__infer_variance__ = bool(infer_variance) + _set_module(tvt) def _typevartuple_prepare_subst(alias, args): @@ -2768,8 +2784,16 @@ def get_shape(self) -> Tuple[*Ts]: def __iter__(self): yield self.__unpacked__ - def __init__(self, name, *, default=NoDefault): + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): self.__name__ = name + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) + if bound: + self.__bound__ = typing._type_check(bound, 'Bound must be a type.') + else: + self.__bound__ = None _DefaultMixin.__init__(self, default) # for pickling: @@ -2780,7 +2804,15 @@ def __init__(self, name, *, default=NoDefault): self.__unpacked__ = Unpack[self] def __repr__(self): - return self.__name__ + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ def __hash__(self): return object.__hash__(self) From 2a3a33bd934138a58fa485d1d562271b9c855be9 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Mon, 15 Jun 2026 01:55:38 +1000 Subject: [PATCH 2/2] fixup! add bound/variance parameters to `TypeVarTuple` --- src/typing_extensions.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 395475d0..3bf50a3e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1750,10 +1750,7 @@ def TypeAlias(self, parameters): def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault - if default is NoDefault: - type_param.__default__ = default - else: - type_param.__default__ = typing._type_check(default, "Default must be a type.") + type_param.__default__ = default def _set_module(typevarlike):