Inconsistent behavior when validating type parameter substitutions
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 77.2k
- Forks
- 35.9k
- PR merge metrics
- PR metrics pending
Description
Bug report
Bug description:
There are currently three different cases (two of them being closely related) where type parameters can be substituted/parameterized with concrete types:
-
On a user-defined generic class:
class MyGeneric[T1, T2]: ... alias = MyGeneric[int, str] -
On an already parameterized alias (following the previous example):
temp_alias = MyGeneric[int, T2] alias = temp_alias[str] -
On a PEP 695 type alias:
type MyAlias[T1, T2] = dict[T1, T2] gen_alias = MyAlias[int, str]
All of these three cases have slightly different behavior. The one that seems to be the most accurate is the second one. As alias is a _GenericAlias instance, parameterizing it will call _GenericAlias.__getitem__. There is a bunch of logic in there, and it seems that both __typing_prepare_subst__ and then __typing_subst__ (which is only doing type check assertions) are being called.
However, the first case is not making the calls to __typing_subst__, meaning the following would unexpectedly work:
class A[T, **P]: ...
A[int, str]
# ok at runtime, should fail as `P` should be substituted with a valid parameter expression
# (another ParamSpec, the ellipsis, a list/tuple of types or a Concatenate form).
If you do the same on a _GenericAlias instance (matches the second case), an error is raised:
alias = A[T, P]
alias[int, str]
# TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got <class 'str'>
This leads us to the first point: should we apply the same logic between these two cases? To avoid breaking changes, we might have to consider forward references:
class A[T, **P]: ...
A[int, 'ForwardParamSpec']
ForwardParamSpec = ParamSpec('ForwardParamSpec')
So perhaps we can exclude ForwardRef from the type check in ParamSpec.__typing_subst__ (note that it already doesn't work for the second case).
I'll also note that having __typing_subst__ not called is not the only difference. For instance, the second case also calls _unpack_args() on the passed args, while the first case doesn't [^1]
Onto the last case (PEP 695 type aliases), currently no validation is performed whatsoever:
type MyAlias[T1, T2] = dict[T1, T2]
MyAlias[int] # no error
So the second point is: should we apply the same logic as in case 1/2? Again, I don't know if applying the same logic here on type aliases is going to introduce any breaking changes concerns?
[^1]: Here is an example of how this manifests: https://gist.github.com/Viicos/db10da58914809a87c0a82c1b2f19162
CPython versions tested on:
3.14
Operating systems tested on:
Linux
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in Lib/typing.py with _GenericAlias.getitem, ParamSpec.typing_subst, and _unpack_args(), then compare those paths with direct generic-class and PEP 695 type-alias substitution. Use the examples in the report to determine whether validation should be consistent, including forward references, and verify the resulting behavior across all three cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100