How to type hint a dictionary with values of different types
Question:
When declaring a dictionary as a literal, is there a way to type-hint what value I am expecting for a specific key?
And then, for discussion: are there guiding principles around dictionary typing in Python? I’m wondering whether it is considered bad practice to mix types in dictionaries.
Here’s an example:
Consider the declaration of a dictionary in a class’s __init__
:
(Disclaimer: I realize in the example, some of the .elements
entries would probably be more appropriate as class attributes, but it’s for the sake of the example.)
class Rectangle:
def __init__(self, corners: Tuple[Tuple[float, float]], **kwargs):
self.x, self.z = corners[0][0], corners[0][1]
self.elements = {
'front': Line(corners[0], corners[1]),
'left': Line(corners[0], corners[2]),
'right': Line(corners[1], corners[3]),
'rear': Line(corners[3], corners[2]),
'cog': calc_cog(corners),
'area': calc_area(corners),
'pins': None
}
class Line:
def __init__(self, p1: Tuple[float, float], p2: Tuple[float, float]):
self.p1, self.p2 = p1, p2
self.vertical = p1[0] == p2[0]
self.horizontal = p1[1] == p2[1]
When I type type the following,
rec1 = Rectangle(rec1_corners, show=True, name='Nr1')
rec1.sides['f...
Pycharm will suggest 'front'
for me. Better still, when I do
rec1.sides['front'].ver...
Pycharm will suggest .vertical
So Pycharm remembers the keys from the dictionary literal declaration in the class’s __init__
, and also their values’ expected types. Or rather: it expects any value to have any one of the types that are in the literal declaration – probably the same as if I had done a self.elements = {} # type: Union[type1, type2]
would do. Either way, I find it super helpful.
If your functions have their outputs type-hinted, then Pycharm will also take that into account.
So assuming that in the Rectangle
example above, I wanted to indicate that pins
is a list of Pin
objects… if pins
was a normal class attribute, it would be:
self.pins = None # type: List[Pin]
(provided the necessary imports were done)
Is there a way to give the same type hint in the dictionary literal declaration?
The following does not achieve what I am looking for:
Add a Union[...]
type hint at the end of the literal declaration?
'area': calc_area(corners),
'pins': None
} # type: Union[Line, Tuple[float, float], float, List[Pin]]
Adding a type-hint to every line:
'area': calc_area(corners), # type: float
'pins': None # type: List[Pin]
}
Is there a best practice for this kind of thing?
Some more background:
I work with Python in PyCharm and I make extensive use of typing, since it helps me to predict and validate my work as I go along. When I create new classes, I also sometimes throw some less frequently used properties into a dictionary to avoid cluttering the object with too many attributes (this is helpful in debug mode).
Answers:
After some more investigation, the best workaround I could find so far was to ensure the the class Pin
could return some kind of a placeholder, and then use the placeholder as a value in the literal declaration.
therefore:
class Pin:
def __init__(self, **kwargs):
if len(kwargs) == 0:
return
Now, in the example in the OP, I could do the following to achieve what I wanted:
...
'area': calc_area(corners),
'pins': List[Pin(),]
}
However, if I had a basic type (or types) as a entries, this would not work.
...
'area': calc_area(corners),
'pins': List[Pin(),]
'color': None
'lap_times': None
}
where
color
expects a string and
lap_times
expects a list with floats…
In such a case the best work-around would be
...
'area': calc_area(corners),
'pins': List[Pin(),]
'color': 'blue'
'lap_times': [0.,]
}
self.elements['color'], self.elements['lap_times'] = None, None
Neither of these seems very elegant, so I am still hoping someone can suggest something better.
You are looking for TypedDict. It is currently only a mypy-only extension, but there are plans to make it an officially sanctioned type in the near-future. I am not sure if PyCharm supports this feature yet, though.
So, in your case, you’d do:
from mypy_extensions import TypedDict
RectangleElements = TypedDict('RectangleElements', {
'front': Line,
'left': Line,
'right': Line,
'rear': Line,
'cog': float,
'area': float,
'pins': Optional[List[Pin]]
})
class Rectangle:
def __init__(self, corners: Tuple[Tuple[float, float]], **kwargs):
self.x, self.z = corners[0][0], corners[0][1]
self.elements = {
'front': Line(corners[0], corners[1]),
'left': Line(corners[0], corners[2]),
'right': Line(corners[1], corners[3]),
'rear': Line(corners[3], corners[2]),
'cog': calc_cog(corners),
'area': calc_area(corners),
'pins': None
} # type: RectangleElements
If you are using Python 3.6+, you can type this all more elegantly using the class-based syntax.
In your specific case though, I think most people would just store those pieces of data as regular fields instead of a dict. I’m sure you’ve already thought through the pros and cons of that approach though, so I’ll skip lecturing you about it.
Easiest way:
from typing import TypedDict
class Movie(TypedDict):
name: str
year: int
movie: Movie = {'name': 'Blade Runner', 'year': 1982}
movie['year'] = '1982' # Error: invalid value type ("int" expected)
When declaring a dictionary as a literal, is there a way to type-hint what value I am expecting for a specific key?
And then, for discussion: are there guiding principles around dictionary typing in Python? I’m wondering whether it is considered bad practice to mix types in dictionaries.
Here’s an example:
Consider the declaration of a dictionary in a class’s __init__
:
(Disclaimer: I realize in the example, some of the .elements
entries would probably be more appropriate as class attributes, but it’s for the sake of the example.)
class Rectangle:
def __init__(self, corners: Tuple[Tuple[float, float]], **kwargs):
self.x, self.z = corners[0][0], corners[0][1]
self.elements = {
'front': Line(corners[0], corners[1]),
'left': Line(corners[0], corners[2]),
'right': Line(corners[1], corners[3]),
'rear': Line(corners[3], corners[2]),
'cog': calc_cog(corners),
'area': calc_area(corners),
'pins': None
}
class Line:
def __init__(self, p1: Tuple[float, float], p2: Tuple[float, float]):
self.p1, self.p2 = p1, p2
self.vertical = p1[0] == p2[0]
self.horizontal = p1[1] == p2[1]
When I type type the following,
rec1 = Rectangle(rec1_corners, show=True, name='Nr1')
rec1.sides['f...
Pycharm will suggest 'front'
for me. Better still, when I do
rec1.sides['front'].ver...
Pycharm will suggest .vertical
So Pycharm remembers the keys from the dictionary literal declaration in the class’s __init__
, and also their values’ expected types. Or rather: it expects any value to have any one of the types that are in the literal declaration – probably the same as if I had done a self.elements = {} # type: Union[type1, type2]
would do. Either way, I find it super helpful.
If your functions have their outputs type-hinted, then Pycharm will also take that into account.
So assuming that in the Rectangle
example above, I wanted to indicate that pins
is a list of Pin
objects… if pins
was a normal class attribute, it would be:
self.pins = None # type: List[Pin]
(provided the necessary imports were done)
Is there a way to give the same type hint in the dictionary literal declaration?
The following does not achieve what I am looking for:
Add a Union[...]
type hint at the end of the literal declaration?
'area': calc_area(corners),
'pins': None
} # type: Union[Line, Tuple[float, float], float, List[Pin]]
Adding a type-hint to every line:
'area': calc_area(corners), # type: float
'pins': None # type: List[Pin]
}
Is there a best practice for this kind of thing?
Some more background:
I work with Python in PyCharm and I make extensive use of typing, since it helps me to predict and validate my work as I go along. When I create new classes, I also sometimes throw some less frequently used properties into a dictionary to avoid cluttering the object with too many attributes (this is helpful in debug mode).
After some more investigation, the best workaround I could find so far was to ensure the the class Pin
could return some kind of a placeholder, and then use the placeholder as a value in the literal declaration.
therefore:
class Pin:
def __init__(self, **kwargs):
if len(kwargs) == 0:
return
Now, in the example in the OP, I could do the following to achieve what I wanted:
...
'area': calc_area(corners),
'pins': List[Pin(),]
}
However, if I had a basic type (or types) as a entries, this would not work.
...
'area': calc_area(corners),
'pins': List[Pin(),]
'color': None
'lap_times': None
}
where
color
expects a string and
lap_times
expects a list with floats…
In such a case the best work-around would be
...
'area': calc_area(corners),
'pins': List[Pin(),]
'color': 'blue'
'lap_times': [0.,]
}
self.elements['color'], self.elements['lap_times'] = None, None
Neither of these seems very elegant, so I am still hoping someone can suggest something better.
You are looking for TypedDict. It is currently only a mypy-only extension, but there are plans to make it an officially sanctioned type in the near-future. I am not sure if PyCharm supports this feature yet, though.
So, in your case, you’d do:
from mypy_extensions import TypedDict
RectangleElements = TypedDict('RectangleElements', {
'front': Line,
'left': Line,
'right': Line,
'rear': Line,
'cog': float,
'area': float,
'pins': Optional[List[Pin]]
})
class Rectangle:
def __init__(self, corners: Tuple[Tuple[float, float]], **kwargs):
self.x, self.z = corners[0][0], corners[0][1]
self.elements = {
'front': Line(corners[0], corners[1]),
'left': Line(corners[0], corners[2]),
'right': Line(corners[1], corners[3]),
'rear': Line(corners[3], corners[2]),
'cog': calc_cog(corners),
'area': calc_area(corners),
'pins': None
} # type: RectangleElements
If you are using Python 3.6+, you can type this all more elegantly using the class-based syntax.
In your specific case though, I think most people would just store those pieces of data as regular fields instead of a dict. I’m sure you’ve already thought through the pros and cons of that approach though, so I’ll skip lecturing you about it.
Easiest way:
from typing import TypedDict
class Movie(TypedDict):
name: str
year: int
movie: Movie = {'name': 'Blade Runner', 'year': 1982}
movie['year'] = '1982' # Error: invalid value type ("int" expected)