An introduction to Python type annotations. Continuation


    Illustration by Magdalena Tomczyk


    In the first part of the article, I described the basics of using type annotations. However, several important points were not considered. Firstly, generics are an important mechanism, and secondly, it can sometimes be useful to find out information about the expected types in runtime. But I wanted to start with simpler things


    Advance announcement


    Usually you cannot use a type before it is created. For example, the following code will not even start:


    class LinkedList:
        data: Any
        next: LinkedList  # NameError: name 'LinkedList' is not defined

    To fix this, it is permissible to use a string literal. In this case, annotations will be calculated deferred.


    class LinkedList:
        data: Any
        next: 'LinkedList'

    You can also access classes from other modules (of course, if the module is imported): some_variable: 'somemodule.SomeClass'


    Comment

    Generally speaking, any computable expression can be used as an annotation. However, it is recommended that they be kept as simple as possible so that the static analysis utilities can use them. In particular, they most likely will not understand dynamically computable types. Read more about restrictions here: PEP 484 - Type Hints # Acceptable type hints


    For example, the following code will work and even annotations will be available in runtime, however mypy will throw an error on it


    def get_next_type(arg=None):
        if arg:
            return LinkedList
        else:
            return Any
    class LinkedList:
        data: Any
        next: 'get_next_type()'  # error: invalid type comment or annotation

    UPD : In Python 4.0, it is planned to enable delayed type annotation calculation ( PEP 563 ), which will remove this technique with string literals. with Python 3.7 you can enable new behavior using the constructfrom __future__ import annotations


    Functions and Called Objects


    For situations when it is necessary to transfer a function or another object we call (for example, as a callback), you need to use the Callable annotation [[ArgType1, ArgType2, ...], ReturnType]
    For example,


    def help() -> None:
        print("This is help string")
    def render_hundreds(num: int) -> str:
        return str(num // 100)
    def app(helper: Callable[[], None], renderer: Callable[[int], str]):
        helper()
        num = 12345
        print(renderer(num))
    app(help, render_hundreds)
    app(help, help)  # error: Argument 2 to "app" has incompatible type "Callable[[], None]"; expected "Callable[[int], str]"

    It is permissible to specify only the return type of the function without specifying its parameters. In this case, the dots: Callable[..., ReturnType]. Note that there are no square brackets around the ellipsis.


    At the moment, it is impossible to describe a function signature with a variable number of parameters of a certain type or specify named arguments.


    Generic Types


    Sometimes it is necessary to save information about a type, without fixing it rigidly. For example, if you write a container that stores the same data. Or a function that returns data of the same type as one of the arguments.


    Types such as List or Callable, which we saw earlier just use the generic mechanism. But besides standard types, you can create your own generic types. To do this, firstly, get TypeVar a variable that will be an attribute of the generic, and secondly, directly declare a generic type:


    T = TypeVar("T")
    class LinkedList(Generic[T]):
        data: T
        next: "LinkedList[T]"
        def __init__(self, data: T):
            self.data = data
    head_int: LinkedList[int] = LinkedList(1)
    head_int.next = LinkedList(2)
    head_int.next = 2  # error: Incompatible types in assignment (expression has type "int", variable has type "LinkedList[int]")
    head_int.data += 1
    head_int.data.replace("0", "1")  # error: "int" has no attribute "replace"
    head_str: LinkedList[str] = LinkedList("1")
    head_str.data.replace("0", "1")
    head_str = LinkedList[str](1)  # error: Argument 1 to "LinkedList" has incompatible type "int"; expected "str"

    As you can see, for generic types, automatic inference of the parameter type works.


    If required, the generic can have any number of parameters: Generic[T1, T2, T3].


    Also, when defining TypeVar, you can restrict valid types:


    T2 = TypeVar("T2", int, float)
    class SomethingNumeric(Generic[T2]):
        pass
    x = SomethingNumeric[str]()  # error: Value of type variable "T2" of "SomethingNumeric" cannot be "str"

    Cast


    Sometimes the analyzer static analyzer cannot correctly determine the type of variable, in this case, you can use the cast function. Its only task is to show the analyzer that the expression is of a particular type. For instance:


    from typing import List, cast
    def find_first_str(a: List[object]) -> str:
        index = next(i for i, x in enumerate(a) if isinstance(x, str))
        return cast(str, a[index])

    It can also be useful for decorators:


    MyCallable = TypeVar("MyCallable", bound=Callable)
    def logged(func: MyCallable) -> MyCallable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(func.__name__, args, kwargs)
            return func(*args, **kwargs)
        return cast(MyCallable, wrapper)
    @logged
    def mysum(a: int, b: int) -> int:
        return a + b
    mysum(a=1)  # error: Missing positional argument "b" in call to "mysum"

    Work with runtime annotations


    Although the interpreter does not use annotations on its own, they are available for your code while the program is running. For this, an object attribute is provided __annotations__that contains a dictionary with the indicated annotations. For functions, these are parameter annotations and return type, for an object, field annotations, for global scope, variables and their annotations.


    def render_int(num: int) -> str:
        return str(num)
    print(render_int.annotations)  # {'num': , 'return': }

    It is also available get_type_hints- it returns annotations for the object passed to it, in many situations it coincides with the content __annotations__, but there are differences: it also adds annotations of the parent objects (in the reverse order __mro__), and also allows preliminary type declarations specified as strings.


    T = TypeVar("T")
    class LinkedList(Generic[T]):
        data: T
        next: "LinkedList[T]"
    print(LinkedList.__annotations__)
    # {'data': ~T, 'next': 'LinkedList[T]'}
    print(get_type_hints(LinkedList))
    # {'data': ~T, 'next': __main__.LinkedList[~T]}

    For generic types, information about the type itself and its parameters is available through attributes __origin__and __args__, but this is not part of the standard and the behavior has already changed between versions 3.6 and 3.7


    Also popular now: