Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeVarTuple and ParamSpec allow too few arguments to be specified when used together #131696

Closed
Viicos opened this issue Mar 24, 2025 · 5 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-typing type-bug An unexpected behavior, bug, or error

Comments

@Viicos
Copy link
Contributor

Viicos commented Mar 24, 2025

Bug report

Bug description:

Following #99344, it seems to me that the following should raise a TypeError:

class A[*Ts, **P]: ...


A[int]  # Should raise
A[int, str]  # Should raise (parameters for **P should be specified as [str], unless P was the only parameter).
 
class B[T, **P]: ...

B[int, str]  # Should also raise

I understand that these use cases may be really hard to support at runtime, if so I don't mind closing the issue.

CPython versions tested on:

3.14

Operating systems tested on:

No response

@Viicos Viicos added the type-bug An unexpected behavior, bug, or error label Mar 24, 2025
@picnixz picnixz added interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-typing labels Mar 24, 2025
@sobolevn
Copy link
Member

sobolevn commented Mar 25, 2025

Right now

class A[*Ts, **P]: ...

A[int]

substitutes *Ts as () and P as [int], which looks kinda correct?

See

cpython/Lib/typing.py

Lines 1085 to 1108 in 6fb5f7f

def _paramspec_subst(self, arg):
if isinstance(arg, (list, tuple)):
arg = tuple(_type_check(a, "Expected a type.") for a in arg)
elif not _is_param_expr(arg):
raise TypeError(f"Expected a list of types, an ellipsis, "
f"ParamSpec, or Concatenate. Got {arg}")
return arg
def _paramspec_prepare_subst(self, alias, args):
params = alias.__parameters__
i = params.index(self)
if i == len(args) and self.has_default():
args = [*args, self.__default__]
if i >= len(args):
raise TypeError(f"Too few arguments for {alias}")
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
if len(params) == 1 and not _is_param_expr(args[0]):
assert i == 0
args = (args,)
# Convert lists to tuples to help other libraries cache the results.
elif isinstance(args[i], list):
args = (*args[:i], tuple(args[i]), *args[i+1:])
return args

_paramspec_prepare_subst args=((), <class 'int'>)
__main__.A[int]

But,

class B[T, **P]: ...

B[int, str]  # Should also raise

clearly looks like a bug to me.

cpython/Lib/typing.py

Lines 1035 to 1041 in 6fb5f7f

def _typevar_subst(self, arg):
msg = "Parameters to generic types must be types."
arg = _type_check(arg, msg, is_argument=True)
if ((isinstance(arg, _GenericAlias) and arg.__origin__ is Unpack) or
(isinstance(arg, GenericAlias) and getattr(arg, '__unpacked__', False))):
raise TypeError(f"{arg} is not valid as type argument")
return arg
is never called for some reason.

It also substitutes as:

_paramspec_prepare_subst args=(<class 'int'>, <class 'str'>)
__main__.B[int, str]

However, B[[int, str]] does not work and raises: TypeError: Too few arguments for <class '__main__.B'>

@Viicos
Copy link
Contributor Author

Viicos commented Mar 25, 2025

Right now

class A[*Ts, **P]: ...

A[int]

ParamSpec should be parameterized with a list/tuple of types, unless the ParamSpec is the only parameter present. This is documented at the end of this section:

Another difference between TypeVar and ParamSpec is that a generic with only one parameter specification variable will accept parameter lists in the forms X[[Type1, Type2, ...]] and also X[Type1, Type2, ...] for aesthetic reasons. Internally, the latter is converted to the former, so the following are equivalent:

>>> class X[**P]: ...
...
>>> X[int, str]
__main__.X[[int, str]]
>>> X[[int, str]]
__main__.X[[int, str]]

So I think in:

class A[*Ts, **P]: ...

A[int]

*Ts indeed substitutes as (), and P as int but this should raise because the convenience of parameterizing with int instead of [int] doesn't apply here.

Valid forms would be:

A[int, [str, int]]
# Ts -> (int,), P -> [str, int]
A[[str, int]]
# TS -> (), P -> [str, int]

@Viicos
Copy link
Contributor Author

Viicos commented Mar 25, 2025

is never called for some reason.

I think this is coming from the _generic_class_getitem() function. It is called in two cases:

  • When you define parameters on your class, i.e. when doing Generic[T, ...] or Protocol[T, ...]. This is covered by the first part of the function:

    cpython/Lib/typing.py

    Lines 1128 to 1135 in 90b82f2

    if is_generic_or_protocol:
    # Generic and Protocol can only be subscripted with unique type variables.
    if not args:
    raise TypeError(
    f"Parameter list to {cls.__qualname__}[...] cannot be empty"
    )
    if not all(_is_typevar_like(p) for p in args):
    raise TypeError(

  • When you parameterize your generic class, i.e. when doing MyClass[int, ...]. This is covered by the second part of the function:

    cpython/Lib/typing.py

    Lines 1141 to 1157 in 90b82f2

    else:
    # Subscripting a regular Generic subclass.
    for param in cls.__parameters__:
    prepare = getattr(param, '__typing_prepare_subst__', None)
    if prepare is not None:
    args = prepare(cls, args)
    _check_generic_specialization(cls, args)
    new_args = []
    for param, new_arg in zip(cls.__parameters__, args):
    if isinstance(param, TypeVarTuple):
    new_args.extend(new_arg)
    else:
    new_args.append(new_arg)
    args = tuple(new_args)
    return _GenericAlias(cls, args)

As we can see, the __typing_prepare_subst__ functions are being called for each parameter, but we don't ever call __typing_subst__ on them.

However, if you parameterize the generic class with the same parameters, this code path will run and you will end up with a _GenericAlias instance:

T = TypeVar('T')
P = ParamSpec('P')

class MyClass(Generic[T, P]): pass

alias = MyClass[T, P]
alias.__class__
#> typing._GenericAlias

And when doing alias[int, str], we will run _GenericAlias.__getitem__() this time, which does implement the necessary checks and calls out __typing_subst__ (_GenericAlias.__getitem__() calls _determine_new_args(), which does similar things as _generic_class_getitem() but also calls _make_substitution(), which has a lot of additional logic).

Things seem to work as expected when parameterizing the alias:

class MyClass(Generic[T, P]): pass

alias = MyClass[T, P]
alias[int]
#> TypeError: Too few arguments for __main__.MyClass[~T, ~P]
alias[int, str]
#> TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got <class 'str'>

class MyClass(Generic[*Ts, P]): pass

alias = MyClass[*Ts, P]
alias[int]
#> TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got <class 'int'>
alias[str, int]
#> TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got <class 'int'>

Before working on fix, I'd like to know if this the omission _generic_class_getitem() is actually not an omission but expected behavior. cc @JelleZijlstra, as you worked PEP 695/696 implementation on this function.

@Viicos
Copy link
Contributor Author

Viicos commented Mar 27, 2025

Actually, that might be too disruptive to add. It might even have been intentional, but this would break use cases where forward references are used when parameterizing classes:

class B[T, **P]: ...

B[int, "ForwardP"]  # If we were to check the class arguments here, this would fail as str is not a list of types,an ellipsis, ParamSpec or Concatenate.

ForwardP = ParamSpec("ForwardP")

Maybe we could try to do the necessary checks, but silently accept strings and forwardrefs?

@Viicos
Copy link
Contributor Author

Viicos commented Apr 4, 2025

I'm closing this one in favor of #132100, which gives more context.

@Viicos Viicos closed this as not planned Won't fix, can't repro, duplicate, stale Apr 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-typing type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants