Skip to content

Commit 6dc02b4

Browse files
committed
Make ReadOnly TypedDict items covariant
Fixes #17901.
1 parent 9e24b56 commit 6dc02b4

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

mypy/subtypes.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -892,15 +892,20 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
892892
return False
893893
for name, l, r in left.zip(right):
894894
# TODO: should we pass on the full subtype_context here and below?
895-
if self.proper_subtype:
896-
check = is_same_type(l, r)
895+
right_readonly = name in right.readonly_keys
896+
if not right_readonly:
897+
if self.proper_subtype:
898+
check = is_same_type(l, r)
899+
else:
900+
check = is_equivalent(
901+
l,
902+
r,
903+
ignore_type_params=self.subtype_context.ignore_type_params,
904+
options=self.options,
905+
)
897906
else:
898-
check = is_equivalent(
899-
l,
900-
r,
901-
ignore_type_params=self.subtype_context.ignore_type_params,
902-
options=self.options,
903-
)
907+
# Read-only items behave covariantly
908+
check = self._is_subtype(l, r)
904909
if not check:
905910
return False
906911
# Non-required key is not compatible with a required key since
@@ -917,7 +922,7 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
917922
# Readonly fields check:
918923
#
919924
# A = TypedDict('A', {'x': ReadOnly[int]})
920-
# B = TypedDict('A', {'x': int})
925+
# B = TypedDict('B', {'x': int})
921926
# def reset_x(b: B) -> None:
922927
# b['x'] = 0
923928
#

test-data/unit/check-typeddict.test

+41
Original file line numberDiff line numberDiff line change
@@ -3988,3 +3988,44 @@ class TP(TypedDict):
39883988
k: ReadOnly # E: "ReadOnly[]" must have exactly one type argument
39893989
[builtins fixtures/dict.pyi]
39903990
[typing fixtures/typing-typeddict.pyi]
3991+
3992+
[case testTypedDictReadOnlyCovariant]
3993+
from typing import ReadOnly, TypedDict, Union
3994+
3995+
class A(TypedDict):
3996+
a: ReadOnly[Union[int, str]]
3997+
3998+
class A2(TypedDict):
3999+
a: ReadOnly[int]
4000+
4001+
class B(TypedDict):
4002+
a: int
4003+
4004+
class B2(TypedDict):
4005+
a: Union[int, str]
4006+
4007+
class B3(TypedDict):
4008+
a: int
4009+
4010+
def fa(a: A) -> None: ...
4011+
def fa2(a: A2) -> None: ...
4012+
4013+
b: B = {"a": 1}
4014+
fa(b)
4015+
fa2(b)
4016+
b2: B2 = {"a": 1}
4017+
fa(b2)
4018+
fa2(b2) # E: Argument 1 to "fa2" has incompatible type "B2"; expected "A2"
4019+
4020+
class C(TypedDict):
4021+
a: ReadOnly[Union[int, str]]
4022+
b: Union[str, bytes]
4023+
4024+
class D(TypedDict):
4025+
a: int
4026+
b: str
4027+
4028+
d: D = {"a": 1, "b": "x"}
4029+
c: C = d # E: Incompatible types in assignment (expression has type "D", variable has type "C")
4030+
[builtins fixtures/dict.pyi]
4031+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)