Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Proper way to use dynamic keys for TypedDict #9168

Open
mortoray opened this issue Jul 18, 2020 · 9 comments
Open

Proper way to use dynamic keys for TypedDict #9168

mortoray opened this issue Jul 18, 2020 · 9 comments

Comments

@mortoray
Copy link

mortoray commented Jul 18, 2020

Is there a way to use dynamic keys for a TypedDict which is properly typed? I have code like this:

if name in ['visible', 'value', 'locked']:
	obj[name] = self.use_var_expr(scope, value)

Where obj is a TypedDict that has the same type for the keys 'visible', 'value', 'locked'.

I know that name is not a literal string, but it's been verified to belong to a set of literal strings that belong to the TypedDict.

@Akuli
Copy link
Contributor

Akuli commented Jul 22, 2020

This should work:

if name in ['visible', 'value', 'locked']:
    name: Literal['visible', 'value', 'locked']
    obj[name] = self.use_var_expr(scope, value)

Here's a somewhat similar situation that I have ran into:

if something: 
    state = 'normal'
else:
    state = 'disabled'
...
obj['thing_that_should_be_normal_or_disabled'] = state

and how to fix that:

state: Literal['normal', 'disabled']
if something: 
    state = 'normal'
else:
    state = 'disabled'
...
obj['thing_that_should_be_normal_or_disabled'] = state

@Akuli
Copy link
Contributor

Akuli commented Jul 22, 2020

ok so my above suggestion doesn't actually work, but this works:

if name in ['visible', 'value', 'locked']:
    name = cast(Literal['visible', 'value', 'locked'], name)
    obj[name] = self.use_var_expr(scope, value)

on python 3.8 or newer: from typing import Literal
older pythons: from typing_extensions import Literal

@mortoray
Copy link
Author

Okay, I'll try that.

Though I don't like the cast, since it's different list of values. so if I happen to mismatch one I might bypass some type safety. Is there any way to convert a literal list of strings for use in the in ... clause?

@Akuli
Copy link
Contributor

Akuli commented Jul 22, 2020

PEP 586 says that "Type checkers may optionally perform additional analysis" to make your code work as is, but mypy doesn't do that: https://www.python.org/dev/peps/pep-0586/#interactions-with-narrowing

I can't figure out a way to do this without repeating the list of strings. I thought about creating a modified version of typing.cast, but mypy special-cases calls to typing.cast and essentially makes it impossible to reimplement typing.cast.

@Akuli
Copy link
Contributor

Akuli commented Jul 22, 2020

For fun, here's a terrible hack that currently works in mypy, and creates an error if the Literal doesn't match keys of the TypedDict. Please don't actually use this anywhere.

from typing import Literal, TypedDict
import typing_inspect   # type: ignore

from typing import cast as literal_cast
def literal_cast(literal, string):    # type: ignore
    assert typing_inspect.is_literal_type(literal)
    assert isinstance(string, str)
    if string in typing_inspect.get_args(literal):
        return string
    return None

class Foo(TypedDict):
    a: str
    b: int
    c: float

typed_dict: Foo = {'a': 'hello', 'b': 1, 'c': 2.34}
string = input("a, b or c: ")
if (checked_string := literal_cast(Literal['a', 'b', 'c'], string)) is not None:
    print(typed_dict[checked_string])
else:
    print("bad input")

@JukkaL
Copy link
Collaborator

JukkaL commented Jul 31, 2020

Inferring literal types in some cases like these might be reasonable I guess, as long as it doesn't seem to generate many false positives (I don't think that it would).

@reinderien
Copy link

reinderien commented Apr 19, 2021

Related: this code should pass -

from typing_extensions import TypedDict, Literal
from typing import Set


class SomeDict(TypedDict):
    key1: int
    key2: int

AllKeys = Literal['key1', 'key2']
s: Set[AllKeys] = {'key1'}
d: SomeDict = {k: 0 for k in s}

since the key type is adequately constrained; but fails with

error: Incompatible types in assignment (expression has type "Dict[Union[Literal['key1'], Literal['key2']], int]", variable has type "SomeDict")

Stranger yet, this can be worked-around by replacing the dictionary comprehension with a naive assignment loop; which then passes.

@mykter
Copy link

mykter commented May 26, 2021

#6262 (comment) would save a lot of duplication if you could define a type that was "a key of this typeddict"

@mardukbp
Copy link

mardukbp commented Apr 6, 2023

Agree. It would be awesome to have something like TypeScript's keyof.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

7 participants