Type resolution for union between typeddict and dict

Here’s a piece of code that passes with mypy, but fails with pyright:

from typing import TypedDict

class Foo(TypedDict):
    bar: int
    baz: list[int]

x: Foo = {"bar": 1, "baz": [1, 2, 3]}
y: Foo = x | {"baz": [1, 2, 3]}  # pyright error

pyright gives the following error:

error: Type "dict[str, object]" is not assignable to declared type "Foo"
    "dict[str, object]" is not assignable to "Foo" (reportAssignmentType)
1 error, 0 warnings, 0 informations 

My question is, is this a bug in pyright? Or is there a scenario where this could lead to unsoundness? I fail to imagine such a scenario.

My mistake - the “bitwise or” confused me. | on two dictionaries creates a union of them.

Yes, I meant to write union, but the official docs for dict doesn’t mention the term. Furthermore, dict do not have an explicit union method, so I decided “bitwise or”. Probably should have added the term “operator”. In any case, let me change the title to union.

This is something that would require special casing in type checkers. So I wouldn’t say it’s a bug in pyright, it just doesn’t go as far as mypy in determining if the Foo type would be preserved in a union with arbitrary dict literals.

dict[str, object] is a correct upper bound on any union with a TypedDict and in fact if you change the second assignment to be incompatible, mypy will give the same error.

You can also make pyright pass by changing bar to NotRequired, since that will make both dict literals a valid Foo on their own.

As far as special casing goes, this should be fairly simple to implement, the inference for preserving the type would just need to be changed to a total=False version of the same TypedDict. So you could open a feature request, if there isn’t already one.

1 Like

Thanks, I have opened a feature request: pyright#9833

Regarding special casing, what do you think about the following?

class FooBase(TypedDict):
    baz: list[int]


class Foo(FooBase):
    bar: int


x: Foo = {"bar": 1, "baz": [1, 2, 3]}
z: FooBase = {"baz": [1, 2, 3]}
y: Foo = x | z  # pyright error, mypy passes

Error:

error: Type "FooBase" is not assignable to declared type "Foo"
    "bar" is missing from "FooBase" (reportAssignmentType)
1 error, 0 warnings, 0 informations 

A union between base TypedDict and child TypedDict should be compatible with child TypedDict. Does this need further special casing? I’m sorry if it’s a dumb question, I know little about type checker implementation.

Technically here mypy is wrong if we only look at the types and disregard the values.

TypedDict is not closed so it provides no guarantees about the types of extra keys, so pyright is correct to complain. FooBase at runtime might contain a bar key which isn’t a int, so it can’t be assigned.

Once PEP-728 is accepted, type checkers could accept this if FooBase were closed or the type of its extra_keys matches the type of bar.

The special casing would be similar in either case, since it’s a pure structural type match.

Ah, I see. This gives me a new perspective on closed. Thanks!