Incorrect type narrowing on union of `TypedDict`
from typing import Any, TypedDict
class _SessionData(TypedDict):
created: int
session: dict[str, Any]
class _EmptyDict(TypedDict):
"""Empty dict for typing."""
SessionData = _SessionData | _EmptyDict
data: SessionData
reveal_type(data)
a = data if data else None
reveal_type(a)
aiohttp_session/__init__.py:56: note: Revealed type is "Union[TypedDict('aiohttp_session._SessionData', {'created': builtins.int, 'session': builtins.dict[builtins.str, Any]}), TypedDict('aiohttp_session._EmptyDict', {})]"
aiohttp_session/__init__.py:58: note: Revealed type is "Union[TypedDict('aiohttp_session._EmptyDict', {}), None]"
I also get the same result if I make it not data. The _SessionData type just disappears for no reason and it keeps saying the empty dict will be the result regardless of the boolean check.
I'm adding mypy to a project just now and it left me wanting a bit around nested TypedDicts. and I think my issue is connected to this one. In describing highly nested objects (e.g. REST API responses), it is often useful to define the levels as TypedDicts in some of which the values might be unions of two other TypedDicts. E.g. like this:
from typing import TypedDict, Union
class InnerA(TypedDict):
common_key: str
class InnerB(TypedDict):
common_key: str
extra_key: int
class Outer(TypedDict):
id: int
inner: list[Union[InnerA, InnerB]]
my_outer: Outer = {
'id': 1,
'inner': [{
'common_key': 'which type could it be?',
'extra_key': 2
}]
}
This trips up mypy, which then complains that Extra key "extra_key" for TypedDict "InnerA".
This trips up mypy, which then complains that
Extra key "extra_key" for TypedDict "InnerA".
Maybe you have a usecase in your code that actually requires it, but that example could just use a single TypedDict with an optional extra_key.
from typing import Union, TypedDict, cast
class DictA(TypedDict):
a: str
class DictAAndB(TypedDict):
a: str
b: str
class DictC(TypedDict):
c: str
def func(foo: Union[DictA, DictAAndB, DictC, None]) -> None:
reveal_type(foo)
# Mypy:
# Union[
# TypedDict('test_mypy.DictA', {'a': builtins.str}),
# TypedDict('test_mypy.DictAAndB', {'a': builtins.str, 'b': builtins.str}),
# TypedDict('test_mypy.DictC', {'c': builtins.str}),
# None,
# ]
# Pyright:
# DictA | DictAAndB | DictC | None
#
# Expected.
assert foo is not None
reveal_type(foo)
# Mypy:
# Union[
# TypedDict('test_mypy.DictA', {'a': builtins.str}),
# TypedDict('test_mypy.DictC', {'c': builtins.str}),
# ]
# Pyright:
# DictA | DictAAndB | DictC
#
# Unexpected behavior from Mypy only. What happened to DictAAndB?
foo = cast(Union[DictA, DictAAndB, DictC], foo)
reveal_type(foo)
# Mypy:
# Union[
# TypedDict('test_mypy.DictA', {'a': builtins.str}),
# TypedDict('test_mypy.DictC', {'c': builtins.str}),
# ]
# Pyright:
# DictA | DictAAndB | DictC
#
# Is there any workaround?
I think this is caused by the same underlying behavior as the OP. Mypy 1.11.1. Pyright narrows the types in exactly the way I expect.
Continuing to try to work around this; it seems I can't even cast my way out. I updated my example in the previous comment.
I ended up switching to Pyright for this project because I couldn't find any way to override Mypy's incorrect narrowing in this situation.