A Dive into Python Type Hints
What are Type Hints?
Type hints allow us to better document the inputs and outputs of our code by labelling the types we expect. They look like this:
# No type hints
def join(separator, items):
return separator.join(items)
# Type hints
def join(separator: str, items: list) -> str:
return separator.join(items)
Above we are specifying the function will take 2 parameters, 1 a string, 1 a list, and will return a string.
They were introduced in Python 3.5, with multiple updates since to the typing
module in the standard library.
Important things to note about type hints:
- They are not enforced at build or run time in any way by Python
- Instead used by type checkers / linters (like mypy or pyright) and IDEs
Type hints can help us catch errors at development time, make the intention of our code clearer and improve our documentation.
Checking Types
If you are using JetBrain's PyCharm, by default it will give you warnings when types don't line up.
VS Code with the Python & PyLance extensions have type checking off by default, enable in the PyLance extension settings by setting the “Type Checking Mode” to basic or strict.
From command line and for CI you can use the mypy or pyright packages.
e.g: running mypy .\type_hints\main.py
Part 1: Going Way Overboard On An "Add" Function
No Type Hinting
This is the function we’ll be using to show various types of type hinting:
def add(left, right):
return left + right
Hinting with Built In Types
Our first example shows the type hinting syntax, this function takes 2 integers and returns an integer:
parameter_name: type
- for inputs-> type
- for return values
def add_1(left: int, right: int) -> int:
return left + right
You can add type hints to variables too:
i: int = 0
The issue with the above code however is that it is too specific; lots of types support +
behaviour.
Yet as we’ve added int
type hints we’ll get a warning if we try to add strings, floats, lists and so on, even though the code would happily add those types together.
Using the Any Type Hint
We could use the Any
type from the typing module, which specifies that we’ll accept literally any type, including None, into the method.
from typing import Any
def add_2(left: Any, right: Any) -> Any:
return left + right
This presents two issues though:
- It’s not really any better than having no type hints, it doesn’t tell us much about the code
- Plenty of types don’t support the
+
operator (such as adict
) so we aren’t catching a possible error here
Using the Union Type Hint
If you want specific a set of types that can be accepted, Union
allows you to do that.
Here we’re saying we will accept either an int
, or float
, or str
for each parameter.
from typing import Union
def add_3(left: Union[int, float, str], right: Union[int, float, str])
-> Union[int, float, str]:
return left + right
This is a little unwieldy though, we have to declare the same Union 3 times.
Instead we can make an alias and reuse it:
from typing import Union
AddTypes = Union[int, float, str]
def add_4(left: AddTypes, right: AddTypes) -> AddTypes:
return left + right
Union helps us declare a set of types, however there are many more types that support +
and we don’t really want to list all of them.
Also, what happens if create our own types that support add (by implementing the __add__
method)?
We would have to go and update the type alias for each class that does this.
Using the Protocol Type Hint
Protocol allows us to specify a type that implements certain behaviours.
Similar to an interface, however the types don’t have to explicitly say they implement the interface.
Here we’re saying that the add function will take in any type that implements the __add__
method.
from typing import Any, Protocol
class SupportsAdding(Protocol):
def __add__(self, added_to) -> Any: ...
def add_5(left: SupportsAdding, right: SupportsAdding) -> Any:
return left + right
Now all these will pass type checking, as int
, str
and list
all implement the __add__
method.
If we were to create a new type that implemented that method, it would pass the checks too.
add_5(1, 2)
add_5("a", "B")
add_5([], [])
A dictionary will fail, as it doesn’t implement an __add__
method.
add_5({"k": 1}, {"k": 2})
Generic Protocols with TypeVar
We can improve this further by restricting it down to the types the __add__
method should use.
As these are dependent on the implementing type, we need to use generics.
Here we are saying that the implemented __add__
method must only add and return the type of the class implementing it.
from typing import Any, Protocol, TypeVar
T = TypeVar("T")
class SupportsAdding(Protocol[T]):
def __add__(self, x: T) -> T: ...
def add_6(left: SupportsAdding, right: SupportsAdding) -> SupportsAdding:
return left + right
Part 2: Other Type Hints Available
Collection Type Hints
There is support for collections in the typing module, for example Dict
, List
, Set
, Tuple
:
from typing import List, Set
# Takes any list, regardless of the types inside it
def collection_add(first: List, second: List) -> List:
return first + second
# Only accepts lists containing strings
def string_collection(items: List[str]) -> List[str]:
return items
# Accepts lists which contain strings, or integers, or both
def bits_n_bobs(items: Set[Union[str, int]]) -> Set[Union[str, int]]:
return items
Dictionary and Tuple Type Hints
Can specify the key and values types in a dictionary, and all value types in a tuple:
from typing import Dict, Set, Tuple
# Dictioary with a string key and integer values
def dictionary(items: Dict[str, int]) -> None:
for k, v in items.items():
print(f"{k} = {v}")
# Tuple where the first value is a string
# the second is an integer and third is a Set of integers
def tupling(my_tuple: Tuple[str, int, Set[int]]) -> None:
name, age, ids = my_tuple
print(f"{name}, {age}, {ids}")
Higher Order Function Hinting
The typing
module also supports defining the shape of a function passed in as a parameter through the Callable
type hint:
from typing import Callable
# This function takes another function as a parameter, then executes it
def logging_wrapper(func: Callable):
print("Starting")
func()
print("Ending")
# This function takes another function as a parameter
# The parameter function must take a string and integer as parameters
# Then must return a string.
def output(func: Callable[[str, int], str]) -> str:
return func("A", 1)
Optional Type Hints
For functions where None can be returned, you could use Union[Type, None]
, however that can be unwieldy.
Instead there is the Optional
type that specifies a value could be either the type or None
.
from typing import List, Optional
T = TypeVar("T")
def get_first_item_or_none(items: List[T]) -> Optional[T]:
return items[0] if items else None
End
Hopefully this was a useful overview of type hinting and the options available in Python from 3.5 onwards.
If you have any questions, thoughts or corrections for the article please feel free to ping me on Twitter here: @LukeAMerrett.
Header image credit: Maëlick In The North