typing
typing copied to clipboard
Should operator `|` on `TypedDict` allow for creating intersection-like dicts?
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 forTypedDict. - 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:
- https://github.com/python/typing/issues/213 (since this feature for
TypedDictprobably falls into the "intersection type" category as well?) - https://github.com/microsoft/pylance-release/issues/2300
- https://github.com/microsoft/pyright/issues/2951
Individual type checkers can decide to add this feature if they choose. I don't think it needs to be spelled out more broadly.
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.
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)
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.
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.