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

Should operator | on TypedDict allow for creating intersection-like dicts? #1445

Open
bluenote10 opened this issue Aug 12, 2023 · 5 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@bluenote10
Copy link

bluenote10 commented Aug 12, 2023

I'm wondering how is the relation between the following PEPs:

  • PEP 584 which introduces operator | for dicts, but doesn't mention how it should behave for TypedDict.
  • PEP 589 which introduces TypedDict, but doesn't mention the operator |.

According to PEP 589, multiple inheritance can be used to create a combined (or "intersection like") dict, and operator | provides the corresponding behavior at runtime. Thus it would be nice if the type system could handle the combination properly (similar to how it is possible in TypeScript):

from typing import TypedDict

class HasFoo(TypedDict):
    foo: int
    
class HasBar(TypedDict):
    bar: int
    
class HasFooAndBar(HasFoo, HasBar):
    ...
    
def f(a: HasFoo, b: HasBar) -> HasFooAndBar:
    return a | b

From a runtime and type-checking perspective this code looks valid, but currently mypy does not accept it (playground):

main.py:15: error: Incompatible return value type (got "HasFoo", expected "HasFooAndBar")  [return-value]
main.py:15: error: Unsupported operand types for | ("HasFoo" and "HasBar")  [operator]

Pyright seems to have the same behavior.

Apparently operator | can only be used for two instances of the same typed dict, which as far as I can see has limited use cases, because using the operator | on two dicts that already have the same fields is kind of pointless (perhaps it mostly makes sense if the type used total=False).

Possible related discussions and issues I've found:

@bluenote10 bluenote10 added the topic: feature Discussions about new features for Python's type annotations label Aug 12, 2023
@JelleZijlstra
Copy link
Member

Individual type checkers can decide to add this feature if they choose. I don't think it needs to be spelled out more broadly.

@erictraut
Copy link
Collaborator

erictraut commented Aug 12, 2023

Both mypy and pyright use the dummy type typing._TypedDict to define the fallback behaviors of a TypedDict. It defines __or__ and __ior__ as follows:

class _TypedDict(Mapping[str, object], metaclass=ABCMeta):
    ...
    if sys.version_info >= (3, 9):
        def __or__(self, __value: typing_extensions.Self) -> typing_extensions.Self: ...
        def __ior__(self, __value: typing_extensions.Self) -> typing_extensions.Self: ...

That's why you see the behavior you do with both pyright and mypy.

It would be possible to override this behavior with a custom signature for these two methods. It would need to produce an intersection type that can't be "spelled" in today's type system, but there is precedent for this (specifically in the isinstance type guard, which can produce intersections).

I would be open to adding this functionality to pyright if there's sufficient interest from pyright users. So far, no one has requested it. AFAIK, no one has requested it in the mypy issue tracker either.

@Gobot1234
Copy link
Contributor

I would be open to adding this functionality to pyright if there's sufficient interest from pyright users. So far, no one has requested it. AFAIK, no one has requested it in the mypy issue tracker either.

FWIW to get around this at least with pyright I just use intersected: ManuallyIntersected = {**typed_dict_1, **typed_dict_2} this feature would be useful in cutting down on a bit of duplication though (and is maybe a bit faster)

@erictraut
Copy link
Collaborator

Ah, that's a good workaround. I don't think it's any faster, since that's effectively what the __or__ method does under the covers. This approach also works with the latest version of mypy thanks to some recent bug fixes in the 1.5 release.

@bluenote10
Copy link
Author

Indeed, the work-around seems to work starting with mypy 1.5.0, i.e., since two days ago (last time I tried it didn't work yet).

This only leaves PEP 584 in a slightly awkward state, because it suggests using operator | as a better/unified alternative to other approaches of merging dicts. But if {**d1, **d2} has better typing support this is a bit unconvincing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

4 participants