mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Cannot turn existing dictionary into TypedDict

Open astrojuanlu opened this issue 4 years ago • 3 comments

I expected any of these forms to be allowed by MyPy, but they aren't:

# typed_dict_test.py
from typing import TypedDict

class D(TypedDict):
    a: str
    b: int


d_untyped = {"a": "foo", "b": 1}

d0 = D({"a": "foo", "b": 1})  # The only one that works
print(d0)

d1 = D(d_untyped)
print(d1)

d2 = D(**d_untyped)
print(d2)

d3 = D(dict(d_untyped))
print(d3)

d4 = D(dict(**d_untyped))
print(d4)
$ python typed_dict_test.py
{'a': 'foo', 'b': 1}
{'a': 'foo', 'b': 1}
{'a': 'foo', 'b': 1}
{'a': 'foo', 'b': 1}
{'a': 'foo', 'b': 1}
$ python -V
Python 3.8.3
$ mypy typed_dict_test.py 
typed_dict_test.py:13: error: Expected keyword arguments, {...}, or dict(...) in TypedDict constructor  [misc]
    d1 = D(d_untyped)
         ^
typed_dict_test.py:16: error: Expected keyword arguments, {...}, or dict(...) in TypedDict constructor  [misc]
    d2 = D(**d_untyped)
         ^
typed_dict_test.py:19: error: Expected keyword arguments, {...}, or dict(...) in TypedDict constructor  [misc]
    d3 = D(dict(d_untyped))
         ^
typed_dict_test.py:22: error: Expected keyword arguments, {...}, or dict(...) in TypedDict constructor  [misc]
    d4 = D(dict(**d_untyped))
         ^
Found 4 errors in 1 file (checked 1 source file)
$ cat mypy.ini 
[mypy]
warn_redundant_casts = True
warn_unused_configs = True
pretty = True
show_error_codes = True

disallow_any_generics = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_unused_ignores = True
warn_return_any = True
no_implicit_reexport = True

I checked that cast(D, ...) works, but I wonder if this is the right way to proceed, or a workaround.

astrojuanlu avatar May 26 '20 07:05 astrojuanlu

While this is difficult in principle if d_untyped is not annotated (because it's inferred as Dict[str,object]), the same errors occur even if it's hand-annotated as D.

This gap makes it quite tough to do more complicated manipulations involving TypedDicts since there's no easy way to express "adding keys to one typeddict to make a different typeddict".

benkuhn avatar Jul 22 '20 14:07 benkuhn

This gap makes it quite tough to do more complicated manipulations involving TypedDicts since there's no easy way to express "adding keys to one typeddict to make a different typeddict".

Indeed, even the following doesn't work:

class D2(D):
    c: str

d5a = D2(d0.copy(), c="bar")
d5b = D2(dict(d0.copy(), c="bar"))
d6a = D2(d0, c="bar")
d6b = D2(dict(d0, c="bar"))

GPHemsley avatar Dec 18 '20 20:12 GPHemsley

If there's any workarounds for this - or even best practices - (perhaps not even using the constructor and using a TypeGuard), feel free to mention as well (and post Python + mypy version)

tony avatar Sep 09 '22 19:09 tony

I just stumbled over the same problem and used this as a solution:

import typing

class D(TypedDict):
    a: str
    b: int

d_untyped = {"a": "foo", "b": 1}

d_typed = typing.cast(D, d_untyped)

See also this blog post: https://adamj.eu/tech/2021/07/06/python-type-hints-how-to-use-typing-cast/

Don´t know, if that is the way to go for now and would be interested in other opinions, too :)

martinbaerwolff avatar Apr 09 '23 02:04 martinbaerwolff