python / mypy

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

TypedDict 'in' narrowing w/o @final #15697

Open ikonst opened 1 year ago

ikonst commented 1 year ago

We shouldn't require @final decoration for TypedDicts to narrow them based on the 'in' operator.

Why?

Basically @erictraut's comment.

In #13838, we've added "key in Union[TypedDict, ...]" narrowing for TypedDicts that are marked @final. The reason was to prevent this:

class Mammal(TypedDict):
  mammary_glands: int

class Bird(TypedDict):
  eggs: int

class Echidna(Mammal):
  eggs: int

animal: Mammal | Bird
if 'eggs' in animal:
  assert_type(animal, Bird)  # WRONG! Could still be a Mammal (a Echidna)
if 'eggs' in animal and 'mammary_glands' in animal:
  assert_never(animal)  # WRONG! Could still be a Mammal (a Echidna)

However, per @erictraut's comment, due to TypedDict being a structural type, we shouldn't consider the class hierarchy when type-matching.

This will be consistent with pyright and TypeScript (Playground Link).

A5rocks commented 1 year ago

Just a note that perhaps, the current behavior can be patched in another (if worse) way?

https://github.com/python/mypy/issues/7981#issuecomment-557079403 as mentioned here, final typeddict semantics was accepted knowing that you wouldn't be able to consider structural subtyping... as far as I read it?

i.e. this should fail (it doesn't right now):

from typing import TypedDict, final

@final
class A(TypedDict):
    x: int

class B(TypedDict):
    x: int
    y: int

y: B = {"x": 42, "y": 42}
x: A = y

... but at the same time https://github.com/python/mypy/pull/15425 was accepted so this new unsafety is probably ok.

ikonst commented 1 year ago

@hauntsaninja Judging by your comment here, let's ship it? :)

It would appear that's also how people expect it to be.

gvanrossum commented 9 months ago

FWIW there are some typos in the example -- mypy doesn't accept it as written. The last lines probably were intended to be:

animal: Mammal | Bird
if 'eggs' in animal:
  assert_type(animal, Bird)  # WRONG! Could still be a Mammal (a Echidna)
if 'eggs' in animal and 'mammary_glands' in animal:
  assert_never(animal)  # WRONG! Could still be a Mammal (a Echidna)
ikonst commented 9 months ago

Thanks @gvanrossum, edited the PR.