Skip to content

Fix TypedDict subtyping #11670

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

Closed
Closed
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
15 changes: 9 additions & 6 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,6 @@ def visit_instance(self, left: Instance) -> bool:
return False
return True
right = self.right
if isinstance(right, TupleType) and mypy.typeops.tuple_fallback(right).type.is_enum:
return self._is_subtype(left, mypy.typeops.tuple_fallback(right))
if isinstance(right, Instance):
if TypeState.is_cached_subtype_check(self._subtype_kind, left, right):
return True
Expand Down Expand Up @@ -297,16 +295,21 @@ def visit_instance(self, left: Instance) -> bool:
return True
if isinstance(item, Instance):
return is_named_instance(item, 'builtins.object')
if isinstance(right, LiteralType) and left.last_known_value is not None:
return self._is_subtype(left.last_known_value, right)
if isinstance(right, CallableType):
# Special case: Instance can be a subtype of Callable.
call = find_member('__call__', left, left, is_operator=True)
if call:
return self._is_subtype(call, right)
return False
else:
return False
# Special cases
if isinstance(right, LiteralType) and left.last_known_value is not None:
return self._is_subtype(left.last_known_value, right)
right_fallback = mypy.typeops.try_getting_instance_fallback(right)
if (right_fallback is not None
and ((isinstance(right, TupleType) and right_fallback.type.is_enum)
or isinstance(right, TypedDictType))):
Copy link
Collaborator

Choose a reason for hiding this comment

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

IIRC the fallback of a TypedDict type is actually not a "real" type, but a sort of implementation artifact. I believe that the actual bug is that we leak the fallback as an instance type. In particular, it shouldn't be possible to bind a TypedDict type to Type[T], since a TypedDict type is not a real class, and doesn't support isinstance, among other things. I suspect that is where things go wrong, and this change would be just papering over the root cause. I'm not 100% sure what is going on here, 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.

the actual bug is that we leak the fallback as an instance type

Yes you're right. I am confused by Type[T] before and I just saw #9773 and other related discussions. If my understanding is correct, these issues should be fiexd by two steps: 1. throw errows when binding a TypedDict to Type[T]. 2. implement TypeForm[t] to support the original design pattern described in those issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oops, I still need some extra investigation to catch up disscussions on Type[T] and TypeForm. It'd be great if you could provide some suggestions on implementation since it's a quite common issue.

return self._is_subtype(left, right_fallback)
return False

def visit_type_var(self, left: TypeVarType) -> bool:
right = self.right
Expand Down
20 changes: 20 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,26 @@ reveal_type(fun(b)) # N: Revealed type is "builtins.object*"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictTempTestName]
from typing import Any, Type, TypeVar
from typing_extensions import TypedDict

T = TypeVar('T')
class C(TypedDict):
x: str

def gen() -> Any:
return {'x': 1}
def foo(cls: Type[T]) -> T:
return gen()
def fun(c: C):
pass

fun(foo(C))
Copy link
Collaborator

Choose a reason for hiding this comment

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

The call foo(C) should actually generate an error (see my other comment). For example, if the call would make sense, we should handle cases like these, which probably won't work correctly right now:

d = foo(C)
d['x'] = 1  # Should be ok
d['y'] = 1  # Should be an error
d['x'] = 'x'  # Should be an error

reveal_type(foo(C)) # N: Revealed type is "__main__.C*"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-full.pyi]

-- Join

[case testJoinOfTypedDictHasOnlyCommonKeysAndNewFallback]
Expand Down