Types#
This page discusses different approaches to annotating code structures.
For more details check the Specification for the Python type system.
import sys
import typing
import typeguard
sys.path.append("/tmp")
Union#
If a value can take multiple types, you have to annotate it as an enumeration of types with | symbol. Using the syntax `typing.Union[<type 1>, <type 2>, …] will have a similar result.
The following cell shows how typeguard.check_type passes the literals 10 and "hello" when comparing with the int | str annotation.
union_type = int | str
typeguard.check_type(10, union_type)
typeguard.check_type("hello", union_type)
'hello'
Note: The typing.Union syntax can look a bit inconvenient if you compare it to enumeration by |, but it has advantage of possibility to list awailable types in the different place.
For example, the following cell shows how you can define typing.Union with types defined as the elements of the list.
types = [int, str, float]
typing.Union[*types]
typing.Union[int, str, float]
Void functions#
If function doesn’t return anything you have to specify None as type of output. In other cases, type analisis tools will allow you to assign the function’s returns to any value - which is incorrect behavior.
The following cell defines the void function and assigns its result to the variable - which is nonsence.
%%writefile /tmp/none_output.py
def fun():
print("test")
val = fun()
Overwriting /tmp/none_output.py
!python3 -m mypy /tmp/none_output.py
Success: no issues found in 1 source file
But mypy sees no problem here - just because the output of the function is not defined.
In contrast, the following cell creates the same file, but function return is annotated as None.
%%writefile /tmp/none_output.py
def fun(val: int) -> None:
print("test")
val = fun(3)
Overwriting /tmp/none_output.py
!python3 -m mypy /tmp/none_output.py
/tmp/none_output.py:4: error: "fun" does not return a value (it only ever returns None) [func-returns-value]
Found 1 error in 1 file (checked 1 source file)
As result mypy returns corresponding error.
No return#
In case some can be stopped during execution without returning anything outside - you must use typing.NoReturn as return type for the function. None is not suitable here because it means that the variable to which the return value is assigned must accept the type None - but it’s not correct if the exception or some other termination function doesn’t return anything.
The following cell creates a function that can return int in some cases or just raise the exception.
%%writefile /tmp/none_output.py
def fun(val: int) -> int | None:
if val < 0:
raise Exception("test")
return 5
val: int = fun(5)
Overwriting /tmp/none_output.py
!python3 -m mypy /tmp/none_output.py
/tmp/none_output.py:6: error: Incompatible types in assignment (expression has type "int | None", variable has type "int") [assignment]
Found 1 error in 1 file (checked 1 source file)
As a result, trying to assign the result of the fun to an integer value will result in an error.
But if you use typing.NoReturn everything works fine.
%%writefile /tmp/none_output.py
from typing import NoReturn
def fun(val: int) -> int | NoReturn:
if val < 0:
raise Exception("test")
return 5
val: int = fun(5)
Overwriting /tmp/none_output.py
!python3 -m mypy /tmp/none_output.py
Success: no issues found in 1 source file
Tuple#
There are significant differences between annotations for lists or sets and annotations for tuples. In tuples, you have to define the type of each element individually, which means you need to count the number of elements in the tuple. However, for lists or sets, it’s sufficient to annotate the types that can be stored in the collection.
The following cell shows that the annotation tuple[int, bool, float, str] does not correspond to the value (10, True, 3.0).
try:
typeguard.check_type(
(10, True, 3.0),
tuple[int, bool, float, str]
)
except Exception as e:
print(e)
tuple has wrong number of elements (expected 4, got 3 instead)
It expects another str value as the last element of the tuple. The following cell compares the tuple (10, True, 3.0, "hello") with the given annotation.
typeguard.check_type(
(10, True, 3.0, "hello"),
tuple[int, bool, float, str]
)
(10, True, 3.0, 'hello')
Now everything is fine.
Any type#
Some times you’ll meet cases where object can take any type. In most cases you can just ignore typing for it. But there are reasons why you should have option to declare that expression can have any type:
To show that any type is a deliberate decision.
To have an option for cases where a type must be specified, such as the type of keys in a dictionary or the type of a particular element in a tuple.
Consider the example where there are functions that return the key of the item with the maximum value from dict. But keys can be of any type that is acceptable to be keys for dictionaries. So it can be completed with syntax below.
from typing import Any
def max_key(inp_dict: dict[Any, int|float]) -> Any:
return max(inp_dict, key=inp_dict.get)
max_key({10: 3, "hello": 7})
'hello'
Sequence#
With typing.Sequence, you can annotate any subscriptable type and those that have a defined __len__ dunder.
The following two cells show the comparison of the list and the tuple with the Sequence annotation.
typeguard.check_type(
(True, "hello", False),
typing.Sequence[str | bool]
)
(True, 'hello', False)
typeguard.check_type(
[True, "hello", False],
typing.Sequence[str | bool]
)
[True, 'hello', False]
Everything works fine. But the following cell shows that the set doesn’t refer to the Sequence.
try:
typeguard.check_type(
{True, "hello", False},
typing.Sequence[str | bool]
)
except Exception as e:
print(e)
set is not a sequence
Typed dictionaries#
By inheriting the typing.TypeDict, you can define a type that behaves like a typed dictionary. For each potential key, you can specify the expected value type.
There are following important details associated with typed dict:
By default, you can add any keys to a
TypedDictinstance. It has to be makred asclosedto prevent this behavior.By default, all attributes specified in the definition of the
TypeDicthaires must be provided, during the creation of an instance. You can regulate this behaviour using:The
totalargument in call definition.The
typing.Required[]ortyping.NotRequired[]qualifiers for the attributes.
The
extra_itemsargument allows you to specify the type of extra values that are not specified in the defition of theTypedDict.You can define a generic
TypedDict. This means that you can specify the type of some elements when creating the instance.
Check more in the TypedDict page.
Consider the following exmaple: The cell defines a class whose instances will behave exactly like a dictionary. However, the value udner the “a” key have to be an integer, and the value under the “b” key have to be a string.
from typing import TypedDict
class MyDict(TypedDict):
a: int
b: str
There is no issue with this during runtime. The following code creates an instance with a string for “a” and an integer for “b”.
MyDict(a="hello", b=20)
{'a': 'hello', 'b': 20}
However, any linting tool will flag this as an error. The following cell applies a typeguard to check that the types are correct:
try:
typeguard.check_type(MyDict(a="value", b=10), MyDict)
except Exception as e:
print(type(e).__name__ + ":", e)
TypeCheckError: value of key 'a' of dict is not an instance of int
Generics#
A generic is a type that can be parametrized with other types. The simpliest and probably the most common generic type is list[int], which means that you are dealing with the list of exactly integers.
In this context, the list annotation is parametrized with int, meaning that any linter or completor treat the elements of the list as integer values.
For more details check the:
Generics page of the official documentation.
Generics section of the
typingpackage.Generics section in the specification of the python typing system.
Generics page on this site.
Most cases of annotations using generics have their own subsection on the page. This section explains the concept of generics and how to create the custom ones.
Annotated#
The typing.Annotated allows the specification of metadata for a variable. This metadata is typically used by frameworks to build a specific patterns that allow to specify how frame work have to deal with the variable.
The following tools are usfull when working with Annotated:
Define the metadate with
Annotated[<type>, <metadata1>, <metadata2>, ...].To load the annotations for the object with metadata use
typing.get_type_hints(<object>, include_extras=True).To access the metadata use, the
__metadata__attribute of thetyping.Annotatedobject.
The following code defines the Example dataclass with x attibute to have "positive" as metadata.
from dataclasses import dataclass
from typing import Annotated, get_type_hints
@dataclass
class Example():
x: Annotated[int, "positive"]
The following cell shows the output of the typing.get_type_hints for the Example class.
hints = get_type_hints(Example, include_extras=True)
hints
{'x': typing.Annotated[int, 'positive']}
And teh way to access exactly metadata.
hints['x'].__metadata__
('positive',)
Consider how metadata can be used. The process function checks whether the x attribute of the passed object has been annotated as positive.
def process(inp):
hints = get_type_hints(type(inp), include_extras=True)
if hints['x'].__metadata__[0] == "positive" and inp.x < 0:
print("Warning")
Therefore, the warning will be printed for the Example instance initialised with a negative x.
process(Example(x=-2))
Warning
But, if the class where x annotated with any other value, everything will be fine.
@dataclass
class Example2():
x: Annotated[int, "any"]
process(Example2(x=-2))