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

Incorrect type narrowing on union of TypedDict #12098

Open
Dreamsorcerer opened this issue Jan 29, 2022 · 5 comments
Open

Incorrect type narrowing on union of TypedDict #12098

Dreamsorcerer opened this issue Jan 29, 2022 · 5 comments
Labels
bug mypy got something wrong topic-type-narrowing Conditional type narrowing / binder topic-typed-dict

Comments

@Dreamsorcerer
Copy link
Contributor

Dreamsorcerer commented Jan 29, 2022

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.

@Dreamsorcerer Dreamsorcerer added the bug mypy got something wrong label Jan 29, 2022
@AlexWaygood AlexWaygood added the topic-type-narrowing Conditional type narrowing / binder label Mar 26, 2022
@korompaiistvan
Copy link

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".

@Dreamsorcerer
Copy link
Contributor Author

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.

@mfisher87
Copy link

mfisher87 commented Aug 16, 2024

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.

@mfisher87
Copy link

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.

@mfisher87
Copy link

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.

mfisher87 added a commit to icesat2py/icepyx that referenced this issue Aug 26, 2024
Mypy doesn't work here because of a bug that can't be worked around:

<python/mypy#12098 (comment)>
mfisher87 added a commit to icesat2py/icepyx that referenced this issue Aug 27, 2024
Mypy doesn't work here because of a bug that can't be worked around:

<python/mypy#12098 (comment)>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-type-narrowing Conditional type narrowing / binder topic-typed-dict
Projects
None yet
Development

No branches or pull requests

5 participants