python / python/mypy

Unions of Literals are not accepted as TypedDict Keys

Open
#16,818 3 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug topic-typed-dict
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal_fail and check_ranges reproductions in the linked Mypy Play example, then compare them with minimal_okay. The change is complete when unions of literal types are accepted as TypedDict keys without the literal-required error, including the nested union case.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.