python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.22k stars 2.78k forks source link

TypedDict annotation keys should considered Literals #16821

Open CarliJoy opened 8 months ago

CarliJoy commented 8 months ago

Feature

TypedDict.__annotations__.keys(), __required_keys__ and __optional_keys__ should evaluate to Sequence of Literals

Pitch

See https://github.com/python/mypy/issues/16813 for an example why this is useful. In general working with TypedDicts in for loop this helps a lot reducing duplicate code / keys.

Probably #16820 and #16819 have to be finished before this becomes useful.

Stealthii commented 3 months ago

Providing an example demonstrating the use of annotation keys as literals when defining type-specific behavior.

from typing import Mapping, TypedDict

class Options(TypedDict, total=False):
    """Options for the example client."""

    entity: str
    example_enabled: bool
    num_requests: int
    timeout: int

def params_to_options(params: Mapping[str, str]) -> Options:
    """Convert query parameters to example options."""
    options: Options = {}

    for key, hint in Options.__annotations__.items():
        if key in params:
            if hint == bool:
                options[key] = params[key] == "true"
            elif hint == int:
                options[key] = int(params[key])
            else:
                options[key] = params[key]

    return options

The above code will pass under pyright, but raise literal-required under mypy.

CarliJoy commented 3 months ago

@Stealthii thats because pyright does not check it all.

This wrong example also passes, even in strict mode:

from typing import Mapping, TypedDict

class Options(TypedDict, total=False):
    """Options for the example client."""

    entity: str
    example_enabled: bool
    num_requests: int
    timeout: int

def params_to_options(params: Mapping[str, str]) -> Options:
    """Convert query parameters to example options."""
    options: Options = {}

    for foo in ["a", "b"]:
        options[foo] = 1

    for key, hint in Options.__annotations__.items():
        key = f"{key}_invalid"
        if key in params:
            if hint == bool:
                options[key] = params[key] == "true"
            elif hint == int:
                options[key] = int(params[key])
            else:
                options[key] = params[key]

    return options

Pyright Play