Skip to content
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

Document TypedDict 'in' narrowing #15695

Open
ikonst opened this issue Jul 17, 2023 · 6 comments
Open

Document TypedDict 'in' narrowing #15695

ikonst opened this issue Jul 17, 2023 · 6 comments

Comments

@ikonst
Copy link
Contributor

ikonst commented Jul 17, 2023

In particular, document the need for @final in order for it to work.

@ikonst ikonst changed the title d Document TypedDict 'in' narrowing Jul 17, 2023
@erictraut
Copy link

erictraut commented Jul 17, 2023

I was the one who originally suggested that this type guard pattern would be safe if the TypedDict class was marked @final, but I recently realized that my reasoning was flawed.

The problem is that @final doesn't make sense here because TypedDict is a structural type like a protocol. The class hierarchy for a structural type is meaningless, and type compatibility is decided on the basis of member types only.

Faced with this realization, I just changed pyright to no longer require @final to apply TypedDict narrowing for an in type guard.

This type guard pattern admittedly opens a small hole in the type system because it's possible that the object at runtime has a key with the name specified in the type guard but still matches the structural definition. In practice, this rarely if ever happens. I verified that TypeScript (whose type system is exclusively structural) supports this same type guard and exhibits the same theoretical hole, but the TypeScript maintainers felt that it was a reasonable and practical tradeoff. I agree with that assessment, so I'm OK supporting this type guard pattern in pyright.

Assuming you agree with my logic, you may want to change mypy's behavior and remove the requirement that the TypedDict is marked @final. If you do this, it will be unnecessary to document this requirement.

@ikonst
Copy link
Contributor Author

ikonst commented Jul 17, 2023

This type guard pattern admittedly opens a small hole in the type system because it's possible that the object at runtime has a key with the name specified in the type guard but still matches the structural definition.

Can you clarify with an example?

@erictraut
Copy link

In this issue, you mentioned that @final is required because "otherwise you can't rule out BookBasedOnMovieResponse". You defined this type as:

class BookBasedOnMovieResponse(BookResponse):
    movie: Movie

You're correct that this definition would not be possible if BookResponse were marked @final because you cannot derive a child class from a parent class that is marked @final. However, this type could be defined as:

class BookBasedOnMovieResponse(TypedDict):
    book: Book
    movie: Movie

The fact that the first definition "derives from" BookResponse is irrelevant because this is a structural type. The class name and hierarchy are not used to determine type compatibility. So these two definitions are the same from a typing perspective.

@ikonst
Copy link
Contributor Author

ikonst commented Jul 18, 2023

Created #15697. We should still document the narrowing itself as an alternative to tagged unions.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Aug 12, 2023

I guess a temptation to be strict comes from the fact that PEP 589 tries vaguely to be strict during construction, in a way that is not really structural:

from typing import TypedDict

class A(TypedDict):
    a: str

class B(A):
    b: int
    
x: A = {"a": "asdf", "b": 5}  # error

Even though:

def takes_a(x: A): ...

def takes_b(x: B):
    takes_a(x)  # no error

I do think the argument for allowing unsoundness here is stronger in TypeScript than it is in Python.

But the ways I've seen people use TypedDict don't seem to be really wanting strong guarantees and IIRC we've already made some unsound concessions around TypedDict creation, so I guess I'm fine with it

@ikonst
Copy link
Contributor Author

ikonst commented Jan 19, 2024

FWIW

from typing import TypedDict

class A(TypedDict):
    a: str

class B(A):
    b: int
    
x: A = {"a": "asdf", "b": 5}  # error

is TypeScript's excess property check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants