From df6ef1fbbad5b81ad7d089748a1a1ca900e93b1b 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/3] add bound/variance parameters to `TypeVarTuple` --- src/typing_extensions.py | 42 +++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 20c331ee..be6b67b7 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2571,20 +2571,32 @@ 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.__covariant__ = covariant + tvt.__contravariant__ = contravariant + tvt.__infer_variance__ = infer_variance + _set_module(tvt) def _typevartuple_prepare_subst(alias, args): @@ -2689,8 +2701,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: @@ -2701,7 +2721,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 7a6e221603cda972a9c711e246edab62f00b2e8b Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:46:09 +1000 Subject: [PATCH 2/3] fixup! add bound/variance parameters to `TypeVarTuple` --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 43 +++++++++++++++++++++++++++++++++-- src/typing_extensions.py | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 733505a5..7231acec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,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 f07e1eb0..4167870d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6552,7 +6552,10 @@ def test_basic_plain(self): 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]') def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): @@ -6750,7 +6753,43 @@ def test_basic_plain(self): 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: + # Not worth creating our own version of TypeVarTuple + # to backport the repr + 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) def test_no_redefinition(self): self.assertNotEqual(TypeVarTuple('Ts'), TypeVarTuple('Ts')) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index be6b67b7..ed1a2a99 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2593,6 +2593,7 @@ def __new__(cls, name, *, bound=None, tvt = typing.TypeVarTuple(name) _set_default(tvt, default) + tvt.__bound__ = bound tvt.__covariant__ = covariant tvt.__contravariant__ = contravariant tvt.__infer_variance__ = infer_variance From 550f1d193c992b858510f356b2a2436e95df9769 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:21:47 +1000 Subject: [PATCH 3/3] fixup! add bound/variance parameters to `TypeVarTuple` --- CHANGELOG.md | 2 +- src/test_typing_extensions.py | 15 ++++++++++----- src/typing_extensions.py | 15 +++++++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7231acec..c4a2232e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,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` +- 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 4167870d..27932b45 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1756,11 +1756,9 @@ 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]], - # Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485 - Unpack[Ts] : Unpack[Ts], } # contains a ForwardRef, TypeVar(~prefix) or no expression do_not_stringify_cases = { @@ -1776,6 +1774,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], } if TYPING_3_10_0: # cannot construct UnionTypes before 3.10 do_not_stringify_cases["str | NoneAlias | StrAlias"] = str | None @@ -6765,8 +6765,9 @@ def test_repr(self): self.assertEqual(repr(Ts_contra), '-Ts_contra') self.assertEqual(repr(Ts_infer), 'Ts_infer') else: - # Not worth creating our own version of TypeVarTuple - # to backport the repr + # 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') @@ -7115,6 +7116,10 @@ def test_typing_extensions_defers_when_possible(self): exclude |= { 'TypeAliasType' } + 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 ed1a2a99..633d3991 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1671,7 +1671,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): @@ -1824,7 +1827,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) @@ -2593,10 +2596,10 @@ def __new__(cls, name, *, bound=None, tvt = typing.TypeVarTuple(name) _set_default(tvt, default) - tvt.__bound__ = bound - tvt.__covariant__ = covariant - tvt.__contravariant__ = contravariant - tvt.__infer_variance__ = infer_variance + 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)