mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Unions of Literals are not accepted as TypedDict Keys

Open CarliJoy opened this issue 1 year ago • 2 comments

Bug Report

If Literals are given as Unions they aren't accepted as TypedDict Keys

To Reproduce

from typing import TypedDict, reveal_type, Final, Literal, TypeAlias, get_args
from datetime import datetime


class RangeInfo(TypedDict):
    begin: datetime
    end: datetime
    created_start: datetime
    created_end: datetime
    # …


TimeRange: TypeAlias = Literal["begin", "end"]
CreatedRange: TypeAlias = Literal["created_start", "created_end"]
RANGES: Final[tuple[tuple[TimeRange | CreatedRange, ...],... ]] = (
    get_args(TimeRange),
    get_args(CreatedRange),
    # … much more range names
)


def check_ranges(option: RangeInfo) -> None:
    """Ensure given datetime ranges are valid"""
    for range_tuple in RANGES:
        reveal_type(range_tuple)  # builtins.tuple[Union[Union[Literal['begin'], Literal['end']], Union[Literal['created_start'], Literal['created_end']]], ...]
        for range_val in range_tuple:
            reveal_type(range_val)  # Union[Union[Literal['begin'], Literal['end']], Union[Literal['created_start'], Literal['created_end']]]
            if not isinstance(option[range_val], datetime):  # ❌  TypedDict key must be a string literal; expected one of ("begin", "end", "created_start", "created_end")  [literal-required]
                raise ValueError(f"{range_val} is not a datetime")
        # … much more checks
        
def minimal_okay(union: Literal["begin"] | Literal["end"], option: RangeInfo) -> datetime:
    return option[union]
    

def minimal_fail(union: TimeRange | CreatedRange, option: RangeInfo) -> datetime:
    return option[union]   # ❌  TypedDict key must be a string literal; expected one of ("begin", "end", "created_start", "created_end")  [literal-required]

MyPy Play

Expected Behavior

There should be no type error, Literals in Unions should be flattened.

I would love if unions of literals are flattened in general. Literal["a"] | Literal["b"] | (Literal["c"] | Literal["d"]) == Literal["a", "b", "c", "d"] But I guess this is yet another issue.

Actual Behavior

Can't access typed dict without type error

Your Environment (see mypy play)

Related #16813

CarliJoy avatar Jan 25 '24 15:01 CarliJoy

It would be nice if mypy becomes smarter about unions of literals but if you want a quick workaround here you can replace TimeRange | CreatedRange by Literal[TimeRange, CreatedRange] (Literal is allowed to contain other literals).

hamdanal avatar Feb 04 '24 19:02 hamdanal

It would be nice if mypy becomes smarter about unions of literals but if you want a quick workaround here you can replace TimeRange | CreatedRange by Literal[TimeRange, CreatedRange] (Literal is allowed to contain other literals).

Good to know, that Literal is allowed to contain other Literals. Thank you.

CarliJoy avatar Feb 05 '24 14:02 CarliJoy