Skip to content

Support type aliases, NamedTuple and TypedDict in constrained TypeVar defaults #18884

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2658,7 +2658,7 @@ def check_typevar_defaults(self, tvars: Sequence[TypeVarLikeType]) -> None:
continue
if not is_subtype(tv.default, tv.upper_bound):
self.fail("TypeVar default must be a subtype of the bound type", tv)
if tv.values and not any(tv.default == value for value in tv.values):
if tv.values and not any(is_same_type(tv.default, value) for value in tv.values):
self.fail("TypeVar default must be one of the constraint types", tv)

def check_enum(self, defn: ClassDef) -> None:
Expand Down
2 changes: 1 addition & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6210,7 +6210,7 @@ def visit_type_var_expr(self, e: TypeVarExpr) -> Type:
):
if not is_subtype(p_default, e.upper_bound):
self.chk.fail("TypeVar default must be a subtype of the bound type", e)
if e.values and not any(p_default == value for value in e.values):
if e.values and not any(is_same_type(p_default, value) for value in e.values):
self.chk.fail("TypeVar default must be one of the constraint types", e)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit strange to have this logic duplicated! Is this because PEP 695 doesn't use TypeVarExpr so checkexpr will never hit this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I wasn't able to get rid of one of these checks, but made a note to myself to try harder later:)

return AnyType(TypeOfAny.special_form)

Expand Down
24 changes: 24 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -2042,3 +2042,27 @@ tuple[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type i
b: tuple[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type is not allowed
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testPEP695TypeVarConstraintsDefaultAliases]
from typing import Generic
from typing_extensions import TypeVar

type K = int
type V = int
type L = list[int]

T1 = TypeVar("T1", str, K, default=K)
T2 = TypeVar("T2", str, K, default=V)
T3 = TypeVar("T3", str, L, default=L)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test also using the type variable? E.g. define generic class using one of the type variables and check how the default works. Not sure if it's important to test it in every test case though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Added basic usage to several tests and also a testcase with py3.13 defaults syntax.


class A1(Generic[T1]):
x: T1
class A2(Generic[T2]):
x: T2
class A3(Generic[T3]):
x: T3

reveal_type(A1().x) # N: Revealed type is "builtins.int"
reveal_type(A2().x) # N: Revealed type is "builtins.int"
reveal_type(A3().x) # N: Revealed type is "builtins.list[builtins.int]"
[builtins fixtures/tuple.pyi]
16 changes: 16 additions & 0 deletions test-data/unit/check-python313.test
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,19 @@ def func_d1(
reveal_type(d) # N: Revealed type is "__main__.A[builtins.float, builtins.str]"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testTypeVarConstraintsDefaultAliasesInline]
type K = int
type V = int

class A1[T: (str, int) = K]:
x: T
class A2[T: (str, K) = K]:
x: T
class A3[T: (str, K) = V]:
x: T

reveal_type(A1().x) # N: Revealed type is "builtins.int"
reveal_type(A2().x) # N: Revealed type is "builtins.int"
reveal_type(A3().x) # N: Revealed type is "builtins.int"
[builtins fixtures/tuple.pyi]
103 changes: 101 additions & 2 deletions test-data/unit/check-typevar-defaults.test
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,6 @@ class C(Generic[_I]): pass
t: type[C] | int = C
[builtins fixtures/tuple.pyi]



[case testGenericTypeAliasWithDefaultTypeVarPreservesNoneInDefault]
from typing_extensions import TypeVar
from typing import Generic, Union
Expand All @@ -749,3 +747,104 @@ MyA = A[T1, int]
a: MyA = A(None, 10)
reveal_type(a.a) # N: Revealed type is "Union[builtins.int, None]"
[builtins fixtures/tuple.pyi]

[case testTypeVarConstraintsDefaultAliasesTypeAliasType]
from typing import Generic
from typing_extensions import TypeAliasType, TypeVar

K = TypeAliasType("K", int)
V = TypeAliasType("V", int)
L = TypeAliasType("L", list[int])
T1 = TypeVar("T1", str, K, default=K)
T2 = TypeVar("T2", str, K, default=V)
T3 = TypeVar("T3", str, L, default=L)

class A1(Generic[T1]):
x: T1
class A2(Generic[T2]):
x: T2
class A3(Generic[T3]):
x: T3

reveal_type(A1().x) # N: Revealed type is "builtins.int"
reveal_type(A2().x) # N: Revealed type is "builtins.int"
reveal_type(A3().x) # N: Revealed type is "builtins.list[builtins.int]"
[builtins fixtures/tuple.pyi]

[case testTypeVarConstraintsDefaultAliasesImplicitAlias]
from typing_extensions import TypeVar

K = int
V = int
L = list[int]
T1 = TypeVar("T1", str, K, default=K)
T2 = TypeVar("T2", str, K, default=V)
T3 = TypeVar("T3", str, L, default=L)
[builtins fixtures/tuple.pyi]

[case testTypeVarConstraintsDefaultAliasesExplicitAlias]
from typing_extensions import TypeAlias, TypeVar

K: TypeAlias = int
V: TypeAlias = int
L: TypeAlias = list[int]
T1 = TypeVar("T1", str, K, default=K)
T2 = TypeVar("T2", str, K, default=V)
T3 = TypeVar("T3", str, L, default=L)
[builtins fixtures/tuple.pyi]

[case testTypeVarConstraintsDefaultSpecialTypes]
from typing import Generic, NamedTuple
from typing_extensions import TypedDict, TypeVar

class TD(TypedDict):
foo: str

class NT(NamedTuple):
foo: str

T1 = TypeVar("T1", str, TD, default=TD)
T2 = TypeVar("T2", str, NT, default=NT)

class A1(Generic[T1]):
x: T1
class A2(Generic[T2]):
x: T2

reveal_type(A1().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': builtins.str})"
reveal_type(A2().x) # N: Revealed type is "Tuple[builtins.str, fallback=__main__.NT]"
[builtins fixtures/tuple.pyi]

[case testTypeVarConstraintsDefaultSpecialTypesGeneric]
from typing import Generic, NamedTuple
from typing_extensions import TypedDict, TypeVar

T = TypeVar("T")

class TD(TypedDict, Generic[T]):
foo: T
class TD2(TD[int]): pass
class TD3(TD[int]):
bar: str

class NT(NamedTuple, Generic[T]):
foo: T
class NT2(NT[int]): pass

T1 = TypeVar("T1", str, TD[int], default=TD[int])
T2 = TypeVar("T2", str, NT[int], default=NT[int])
T3 = TypeVar("T3", str, TD2, default=TD[int])
T4 = TypeVar("T4", str, TD3, default=TD[int]) # E: TypeVar default must be one of the constraint types
T5 = TypeVar("T5", str, NT2, default=NT[int]) # E: TypeVar default must be one of the constraint types

class A1(Generic[T1]):
x: T1
class A2(Generic[T2]):
x: T2
class A3(Generic[T3]):
x: T3

reveal_type(A1().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': builtins.int})"
reveal_type(A2().x) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.NT[builtins.int]]"
reveal_type(A3().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': builtins.int})"
[builtins fixtures/tuple.pyi]