Annotation cases#

This page considers details of annotation in some special cases.

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.

typing.NoReturn#

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