python / python/mypy

Precise typing for TypedDict wrappers

Open
#7,856 6 comments 7 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature priority-1-normal topic-typed-dict
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug or feature request: feature request (I think)

I tried to use TypedDict for my codebase and ran into some issues. Consider the following code snippet:

from typing import Any, Union, Optional
from typing_extensions import Literal
from mypy_extensions import TypedDict


class Test(TypedDict):
    a: str
    b: int
    c: int


TestKeys = Literal["a", "b", "c"]  # QUESTION: is there a Test.KEYS or something equivalent?
TestValue = Union[str, int]  # QUESTION: is there a Test.VALUE_TYPES or something equivalent?


class Foo:
    def __init__(self):
        self._foo: Optional[Test] = None

    def _set(self) -> None:
        self._foo = {"a": "a", "b": 0, "c": 1}

    def __getitem__(self, key: TestKeys) -> Any:
        self._set()
        assert self._foo is not None
        return self._foo[key]

    def a(self) -> str:
        return self["a"]

    def b(self) -> int:
        return self["b"]


class Bar:
    def __init__(self):
        self._bar: Optional[Test] = None

    def _set(self) -> None:
        self._bar = {"a": "a", "b": 0, "c": 1}

    def __getitem__(self, key: TestKeys) -> TestValue:
        self._set()
        assert self._bar is not None
        return self._bar[key]

    def a(self) -> str:
        return self["a"]  # WRONG: Incompatible return value type (got "Union[str, int]", expected "str")

    def b(self) -> int:
        return self["b"]  # WRONG: Incompatible return value type (got "Union[str, int]", expected "int")


a = Foo().a()
a.find("a")

b = Foo().b()
b.find("a")  # CORRECT: "int" has no attribute "find"

c = Foo()["a"]
c.find("a")

d = Foo()["b"]
d.find("a")  # MISSING: no error reported here (if self._foo is not lazily initialized the error gets correctly reported)

e = Bar().a()
e.find("a")

f = Bar().b()
f.find("a")  # CORRECT: "int" has no attribute "find"

g = Bar()["a"]
g.find("a")  # WRONG: Item "int" of "Union[str, int]" has no attribute "find"

h = Bar()["b"]
h.find("a")  # WRONG: Item "int" of "Union[str, int]" has no attribute "find" (should be "int" has no attribute "find")

(Python 3.8.0, mypy==0.740, mypy-extensions==0.4.3)

I'm not sure what the correct return type signature for a wrapping __getitem__ would be. Both Any and Union seem to erase the actual type coming from the TypedDict and lead to incorrect results. Is there currently a way to achieve the desired behavior here? Also, is there a way to specify Literals for the keys without manually copying them?

Something like

KT = TypeVar("KT", bound=TestKeys)  # or better Test.KEYS

def __getitem__(self, key: KT) -> Test[KT]:
    ...

would be cool if it worked.

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

Use the TypedDict wrapper example in the issue as the reproduction, focusing on generic getitem return types, key literals, and lazy initialization. Compare the reported and expected diagnostics for the a, b, and indexed-access examples; done means precise value inference without manually duplicating TypedDict keys or types.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.