Daniel Kats
Daniel Kats

Reputation: 5554

mypy set dictionary keys / interface

Suppose I have a function which takes a dictionary as a parameter:

def f(d: dict) -> None:
    x = d["x"]
    print(x)

Can I specify that this dictionary must have the key "x" to mypy? I'm looking for something similar to interface from typescript, without changing d to a class.

The reason I don't want to change d to a class, is because I am modifying a large existing codebase to add mypy type checking and this dictionary is used in many places. I would have to modify a lot of code if I had to change all instances of d["x"] to d.x.

Upvotes: 5

Views: 2392

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1121206

As of Python 3.8 you can use typing.TypedDict, added as per PEP 589. For older Python versions you can use the typing-extensions package

Note that the PEP does acknowledge that the better option is that you use dataclasses for this use-case, however:

Dataclasses are a more recent alternative to solve this use case, but there is still a lot of existing code that was written before dataclasses became available, especially in large existing codebases where type hinting and checking has proven to be helpful.

So the better answer is to consider a different data structure, such as a named tuple or a dataclass, where you can specify the attribute a type has. This is what the typescript declaration does, really:

The printLabel function has a single parameter that requires that the object passed in has a property called label of type string.

Python attributes are the moral equivalent of Typescript object properties. That Typescript object notation and Python dictionaries have a lot in common perhaps confuses matters, but you should not look upon Typescript object declarations as anything but classes, when trying to map concepts to Python.

That could look like this:

from dataclasses import dataclass

@dataclass
class SomeClass:
    x: str

def f(sc: SomeClass) -> None:
    x = sc.x
    print(x)

That said, you can use typing.TypedDict here:

from typing import TypedDict

class SomeDict(TypedDict):
    x: str

def f(d: SomeDict) -> None:
    x = d['x']
    print(x)

Keys in a TypeDict declaration are either all required, or all optional (when you set total=False on the declaration); you'd have to use inheritance to produce a type with some keys optional, see the documentation linked. Note that TypedDict currently has issues with a mix of optional and required keys; you may want to use the typing-extensions package to get the Python 3.9 version (which fixes this) as a backport even when using Python 3.8. Just use from typing_extensions import TypedDict instead of the above from typing ... import, the typing-extensions package falls back to the standard library version when appropriate.

Upvotes: 5

ethanhs
ethanhs

Reputation: 1843

Mypy extends PEP 484 by providing a TypedDict type. This allows specifying specific attributes of a dict type. In your case you can do the following:

from mypy_extensions import TypedDict

# you can also do HasX = TypedDict('HasX', {'x': str})
class HasX(TypedDict):
    x: str

def f(x: HasX) -> None:
    reveal_type(d["x"])  # error: Revealed type is 'builtins.str'

Upvotes: 3

Related Questions