python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.55k stars 2.84k forks source link

`arg-type` error duck typing a `TypedDict` with `NotRequired` field #18162

Closed ZeeD closed 6 days ago

ZeeD commented 1 week ago

Bug Report

mypy play url

it seems that mypy doesn't "match" a NotRequired field with a normal one when duck typing 2 different typed dict:

Let's say that I have a function foo that expect a Small TypedDict as parameter

class Small(TypedDict):
    a: NotRequired[str]

def foo(small: Small) -> None: ...

In this Small TypedDict the field a is marked as NotRequired.

Elsewere I have another Big TypedDict with 2 fields a, and b, both required.

class Big(TypedDict):
    a: str
    b: str

but if I try to pass a Big dict instance to my foo function, mypy mark it as an error: main.py:21: error: Argument 1 to "foo" has incompatible type "Big"; expected "Small" [arg-type]

If I change the type of Big.a to a NotRequired[str], mypy is happy, however mypy should allow the original definition, as a "normal" field should be treated as a stricter, "smaller" version of a not required one

brianschubert commented 1 week ago

One wrinkle is that Small allows deleting keys whereas Big does not, which makes passing a Big where a Small is expected potentially unsound.

Consider:

from typing import NotRequired, TypedDict

class Big(TypedDict):
    a: str
    b: str

class Small(TypedDict):
    a: NotRequired[str]

def foo(small: Small) -> None:
    del small["a"]  # ok, deleting NotRequired keys is permitted

x = Big(a="a", b="b")
foo(x)  # danger! If this was legal, x would no longer be a valid Big

However, if you prevent key deletion using ReadOnly, I think this would be safe. Pyright seems to support this, provided that the NotRequired fields are ReadOnly: [pyright playground]

ZeeD commented 6 days ago

Thanks!