Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Using variables as key of TypedDict #7178

Closed
RonNabuurs opened this issue Jul 9, 2019 · 19 comments
Closed

Using variables as key of TypedDict #7178

RonNabuurs opened this issue Jul 9, 2019 · 19 comments

Comments

@RonNabuurs
Copy link

With the following example I get an error.

from mypy_extensions import TypedDict

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

def read(test: Test) -> str:
    key = 'a'
    return test[key]
error: TypedDict key must be a string literal; expected one of ('a', 'b')

Can mypy not figure out that key is set to a literal?

In my own code im building up key dynamically to get a key from a TypedDict.

mypy version: 0.711

@msullivan
Copy link
Collaborator

We don't support that, no. I think it does work if key is given a Literal type, but that doesn't seem super useful.

The dynamic version we definitely won't be able to do useful typechecking of, I think. Best move is probably a # type: ignore

@ilevkivskyi
Copy link
Member

I think key: Final = 'a' should also work, if this helps.

@b0g3r
Copy link

b0g3r commented Oct 4, 2019

Should, but doesn't :(

In my case, I restrict possible keys for external API, e.g.:

class DataType(TypedDict, total=False):
    FIELD1: str
    FIELD2: str

def send_to_external_api(data: DataType):
   ...

And then use these keys in generic way:

class MyGenericWay:
    external_api_key: Final = 'FIELD1'

    def send(self):
        send_to_external_api({self.external_api_key: 'value'})

class AnotherClass:
    external_api_key: Final = 'FIELD2'
    ...

But mypy allows only in-place literal as keys and also restrict Final/Literal:

from mypy_extensions import TypedDict
from typing_extensions import Literal, Final


class MyDict(TypedDict):
    field_name: int

d: MyDict
d = {'field_name': 1}

name_in_var = 'field_name'
d = {name_in_var: 1}

name_in_literal2: Literal['field_name'] = 'field_name'
d = {name_in_literal2: 1}

name_in_final: Final = 'field_name'
d = {name_in_final: 1}
test.py:13: error: Expected TypedDict key to be string literal
test.py:16: error: Expected TypedDict key to be string literal
test.py:19: error: Expected TypedDict key to be string literal
test.py:22: error: Expected TypedDict key to be string literal

Should I create a new issue, or @msullivan will reopen this?

@b0g3r
Copy link

b0g3r commented Oct 5, 2019

Looks like it needs to change a few lines in checkexpr::check_typeddict_call_with_dict:

mypy/mypy/checkexpr.py

Lines 495 to 500 in bd00106

for item_name_expr, item_arg in kwargs.items:
if not isinstance(item_name_expr, StrExpr):
key_context = item_name_expr or item_arg
self.chk.fail(message_registry.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, key_context)
return AnyType(TypeOfAny.from_error)
item_names.append(item_name_expr.value)

Add check for literal like this:

if not isinstance(item_name_expr, StrExpr) or not (isinstance(item_name_expr, LiteralType) and isinstance(item_name_expr.value, str)):

@ilevkivskyi What do you think? I can create a new PR for this issue if you think it's a good idea to implement and I use the right way to check LiteralType 🤔

@ilevkivskyi
Copy link
Member

@b0g3r Your use case is a different issue, it will be fixed by #7645

@svet-b
Copy link

svet-b commented Aug 8, 2022

This is problematic for us, since we use "constants" [0] like SOME_KEY = 'some_key' to do dictionary lookups in a predictable way across our codebase. Doing this generally reduces the chance of runtime failure due to typos in key names, and even has benefits like enabling auto-completion (since key names are pre-defined). But it doesn't play nice with mypy, as others have described above, which is a shame.

That said, I did find that setting SOME_KEY: Final = 'some_key' seems to allow the use of variables as keys. This is on mypy version 0.971.

What still doesn't work is something like the following:

from typing import Final, TypedDict

class A(TypedDict):
    key1: str
    key2: str

KEY1: Final = 'key1'
KEY2: Final = 'key2'

a = A({KEY1: 'abc', KEY2: 'def'})

ALLOWED_KEYS: Final = (KEY1, KEY2)
for k in ALLOWED_KEYS:
    print(a[k])
mypy_test.py:14: error: TypedDict key must be a string literal; expected one of ("key1", "key2")

[0] These are of course not real constants since that's not a thing, but they're variables that never change.

@AlexWaygood
Copy link
Member

AlexWaygood commented Aug 8, 2022

What still doesn't work is something like the following:

from typing import Final, TypedDict

class A(TypedDict):
    key1: str
    key2: str

KEY1: Final = 'key1'
KEY2: Final = 'key2'

a = A({KEY1: 'abc', KEY2: 'def'})

ALLOWED_KEYS: Final = (KEY1, KEY2)
for k in ALLOWED_KEYS:
    print(a[k])

Here's a workaround for you that might help:

from typing import TypedDict, Sequence, Literal

class A(TypedDict):
    key1: str
    key2: str

a = A({KEY1: 'abc', KEY2: 'def'})

ALLOWED_KEYS: Sequence[Literal['key1', 'key2']] = (KEY1, KEY2)
for k in ALLOWED_KEYS:
    print(a[k])

https://mypy-play.net/?mypy=latest&python=3.10&gist=4cf83edbe0afb7d5de7e4de44fb0c404

@svet-b
Copy link

svet-b commented Aug 8, 2022

Thanks @AlexWaygood , appreciate the tip! Thought I can't shake the feeling that this level of verbosity is not justified here. Specifically, I can't execute

ALLOWED_KEYS: Sequence[Literal['key1', 'key2']] = (KEY1, KEY2)

without first defining KEY1 and KEY2 and giving them values. So in this line I'm providing those values again, purely for type-checking purposes. Which also introduces the risk that the key values used for type-checking are actually different from the real variable values. Though maybe I'm missing something.

Either way, I think the above is maybe a bit of an edge case to the core issue outlined by OP. Though it would be nice if cases like these - which are quite unambiguously correct from a developer perspective - are handled properly by mypy. Otherwise it's easy to feel that we have to jump through hoops and make the code less readable just in order to satisfy mypy.

@qeternity
Copy link

Why is this closed? It's not resolved and it's very awkward when using TypedDicts.

@ketozhang
Copy link

ketozhang commented Sep 21, 2022

Furthermore, a common use case is converting dict -> tuple of a specific order (e.g., passing kwargs to position-only function):

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

d: D =  {"a": 0, "b": "foo"}

key_order = ("b", "a")
t = (d[k] for k in key_order)  # error: TypedDict key must be a string literal; expected one of ("a", "b")

@gandhis1
Copy link

gandhis1 commented Sep 21, 2022

Here's another example that I don't think should error:

from typing import TypedDict

DataDict = TypedDict(
    "DataDict",
    {
        "A": float,
        "B": float,
    },
)

test: DataDict = {"A": 1.0, "B": 2.0}
for field in ["A", "B"]:
    test[field] *= 2.0  # TypedDict key must be a string literal; expected one of ("A", "B")

I echo previous thoughts about this issue needing remain open as the problem is not resolved.

pabluk added a commit to Boavizta/environmental-footprint-data that referenced this issue Oct 15, 2022
As described on python/mypy#7178 (comment)
isn't possible to use dynamic keys with TypedDict and the recommendation
is to use `# type: ignore [misc]`.
AirLoren pushed a commit to Boavizta/environmental-footprint-data that referenced this issue Oct 18, 2022
* Fix error: Value of type "Optional[Match[Any]]" is not indexable

mypy throws this error because the 2nd lambda function doesn't
handle the case of re.search() returning None.
The `get_key` function also requires two `assert` statements to
provide more hints to mypy and keep the previous behaviour when:

 1. `key_name` doesn't exist into `device`
 2. re.search() will always find a group (`0`)

* Fix error: Library stubs not installed for "requests"

* Fix error: Need type annotation for "pie_data"

* Fix error: Need type annotation for "report"

* Fix error: Item "float"/"int" of "Union[float, str, int]" has no attribute "replace"

* Ignore error: TypedDict key must be a string literal

As described on python/mypy#7178 (comment)
isn't possible to use dynamic keys with TypedDict and the recommendation
is to use `# type: ignore [misc]`.

* Fix error: Value of type variable "AnyStr" of "search" cannot be "Union[float,str]"

* Ignore error: Value of type "Optional[Match[str]]" is not indexable

If the result of re.search()[0] is not indexable (returns `None`) it will
be handled by the try/except block, so it can be safely ignored.
But maybe it could be rewritten to handle this in a clear way.

* Fix error: Need type annotation for "seen"

Add type annotation for `seen` variable.
@aarondr77
Copy link

Echoing the sentiment that this would be a helpful improvement. We use constants exactly as @svet-b described above.

@rd-andreas-lay
Copy link

Same here. It's confusing and would be highly appreciated if fixed.

@JanEricNitschke
Copy link

JanEricNitschke commented Jan 12, 2023

I would also really like this to be adressed. I have two related problems:

from typing import TypedDict

class GameRound(TypedDict):

    roundNum: int
    tRoundSpendMoney: int
    tBuyType: str

cols = [
    "roundNum",
    "tRoundSpendMoney",
    "tBuyType",
]
r: GameRound = {"roundNum": 5, "tRoundSpendMoney": 1000, "tBuyType":"Eco"}

for k in cols:
     print(r[k])  # TypedDict key must be a string literal; expected one of ("roundNum", "tRoundSpendMoney", "tBuyType")  [literal-required]

for k in r.keys():
    print(r[k])  # TypedDict key must be a string literal; expected one of ("roundNum", "tRoundSpendMoney", "tBuyType")  [literal-required]
    
for k in r:
    print(r[k])  # TypedDict key must be a string literal; expected one of ("roundNum", "tRoundSpendMoney", "tBuyType")  [literal-required]

Mypy should be able to figure out that k is always a valid key for r or there should at least be a way to tel it that without having to manually type the keys multiple times like in a cast. Because the real GameRound dict does not just have 3 keys but ~20

l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 15, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 15, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 15, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 15, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 15, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 16, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 16, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 16, 2023
Can't pull out image type because of
<python/mypy#7178>.
l0b0 added a commit to linz/emergency-management-tools that referenced this issue Jan 16, 2023
Can't pull out image type because of
<python/mypy#7178>.
kodiakhq bot pushed a commit to linz/emergency-management-tools that referenced this issue Jan 16, 2023
Can't pull out image type because of
<python/mypy#7178>.
@owocado
Copy link

owocado commented Feb 5, 2023

+1 I would find this really useful if this can be address and fixed.
Making TypedDicts work with mypy is really painful right now, especially when your use-case is to use instance of TypedDict in a for-loop/list comprehension when key/value can be dynamic. It would be useful if we end-users can tell mypy to be silent on such cases through a configuration setting. Thank you for reading.

ilevkivskyi added a commit that referenced this issue Feb 7, 2023
Ref #7178

This code is used for some TypedDict errors, but `misc` was used for
others. I make it more consistent. Also this code looks undocumented, so
I add some basic docs.
@stefanmatar
Copy link

Doesn't seem to have been fully fixed by #14621, (should be included in the latest mypy version 1.4.1) given this definiton:

TYPE_A = "TYPE_A"
TYPE_B = "TYPE_B"
TYPE_C = "TYPE_C"

Types = Literal['TYPE_A', 'TYPE_B', 'TYPE_C']


class BaseMongoDto(TypedDict):
    type: Types


class SomeMongoDto(BaseMongoDto):
    id: UUID


instance = SomeMongoDto(
    type=TYPE_A,
    ...
)

I am getting mongodb/mappers.py:92: error: Incompatible types (expression has type "str", TypedDict item "type" has type "Literal['TYPE_A', 'TYPE_B', 'TYPE_C']") [typeddict-item]

And I need to add # type: ignore[typeddict-item] to ignore the error.

@tibbe
Copy link

tibbe commented Jul 18, 2023

Can confirm it's not fixed in 1.4.1, even if the key is a known Literal.

@ikonst
Copy link
Contributor

ikonst commented Jul 18, 2023

Given the amount and persistency of feedback, looks to me like a case of you're holding it wrong. Regardless of what's the right principled approach, there's obviously a usability issue.

While we document how to define "constants", the behavior is apparently still unintuitive, should we consider solutions here?

e.g.

  • aligning with pyright?, or
  • in the case of the expression being a name referencing a variable but not an argument, add a hint "did you forget to add Final?"

@loxosceles
Copy link

Why is this actually closed? I can't find any hint of that being fixed now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests