Сообщить об ошибке.

Аннотации типов в Python

Аннотации типов в Python являются полностью необязательной информацией метаданных о типах, используемых пользовательскими функциями.

Аннотации хранятся в атрибуте функции __annotations__ как словарь и не влияют ни на какую другую часть функции. Аннотации аргументов определяются двоеточием : после имени параметра/аргумента, за которым следует выражение, оценивающее значение аннотации. Аннотации возвращаемых значений функции определяются литералом ->, за которым следует выражение, между списком параметров и двоеточием, обозначающим конец оператора def.

Этот материал представляет собой краткую шпаргалку, показывающую, как использовать аннотации типов для различных распространенных типов в Python.

Мотивация

Добавление аннотаций типов обычно ничего не меняет по сравнению с тем, что произошло бы без неё. Но теперь, в процессе разработки, но уже с аннотациями, при активации автодополнения с помощью Ctrl+Space можно увидеть существующие в коде переменные/классы/функции с принимаемыми/возвращаемыми типами.

Например следующая функция уже имеет аннотации типов.

def get_name_age(name: str, age: int) -> str:
    name_age = name + " is this old: " + age
    return name_age

Так как редактор знает типы переменных, то получаем не только дополнение, но и проверку ошибок:

Если конечно установлен модуль mypy и настроен редактор кода, например Visual Studio Code.

def get_name_with_age(name: str, age: int) -> str:
    # исправляем `age` на `str(age)`
    name_with_age = name + " is this old: " + str(age)
    return name_with_age

Аннотация переменных в Python

Обратите внимание, что Python обычно может определить тип переменной по ее значению, поэтому технически эти аннотации являются избыточными

# объявляем тип переменной
age: int = 1

# не нужно инициализировать переменную, чтобы аннотировать ее.
a: int  # Ok

# может быть полезно в условиях
child: bool
if age < 18:
    child = True
else:
    child = False

Полезные встроенные типы

Для большинства типов можно просто использовать название типа в аннотации:

x: int = 1
x: float = 1.0
x: bool = True
x: str = "test"
x: bytes = b"test"

Generic-типы с параметрами типов

Для коллекций на Python 3.9+ тип элемента коллекции указан в скобках:

x: list[int] = [1]
x: set[int] = {6, 7}

Для словарей необходимо указывать типы ключей и значений:

x: dict[str, float] = {"field": 2.0}  # Python 3.9+

Для кортежей фиксированного размера указываем типы всех элементов:

x: tuple[int, str, float] = (3, "yes", 7.5)  # Python 3.9+

Для кортежей переменного размера используем один тип и многоточие:

x: tuple[int, ...] = (1, 2, 3)  # Python 3.9+

В Python 3.8 и более ранних версиях имя типа коллекции пишется с заглавной буквы, а тип импортируется из модуля typing:

from typing import List, Set, Dict, Tuple

x: List[int] = [1]
x: Set[int] = {6, 7}
x: Dict[str, float] = {"field": 2.0}
x: Tuple[int, str, float] = (3, "yes", 7.5)
x: Tuple[int, ...] = (1, 2, 3)

В Python 3.10+ можно использовать оператор |, когда что-то может быть одним из нескольких типов:

x: list[int | str] = [3, 5, "test", "fun"]  # Python 3.10+

В более ранних версиях Python нужно использовать typing.Union

from typing import Union

x: list[Union[int, str]] = [3, 5, "test", "fun"]

Используйте Optional[X] для значения, которое может быть None. Optional[X] аналогичен записи X | None или Union[X, None]

from typing import Optional

x: Optional[str] = "something" if some_condition() else None

if x is not None:
    # Python понимает, что `x` здесь не будет `None` из-за оператора if
    print(x.upper())

# Если значение никогда не может быть `None` из-за какой-то логики, 
# которую Python не понимает, то используйте утверждение
assert x is not None
print(x.upper())

Аннотация функций в Python

Аннотация определения функции:

def stringify(num: int) -> str:
    return str(num)

Аннотация нескольких аргументов функции:

def plus(num1: int, num2: int) -> int:
    return num1 + num2

Если функция не возвращает значение, то используем None в качестве возвращаемого типа. Значение по умолчанию для аргумента идет после аннотации типа.

def show(value: str, excitement: int = 10) -> None:
    print(value + "!" * excitement)

Обратите внимание, что аргументы без типа являются динамически типизированными (трактуются как typing.Any) и что функции без аннотаций не проверяются

def untyped(x):
    x.anything() + 1 + "string"  # никаких ошибок

Аннотация вызываемых значений аргументов:

from typing import Callable

x: Callable[[int, float], float] = f
def register(callback: Callable[[str], int]) -> None: ...

Функция-генератор, возвращающая целые числа, для аннотирования является просто функцией, которая возвращает итератор целых чисел:

from typing import Iterator

def gen(n: int) -> Iterator[int]:
    i = 0
    while i < n:
        yield i
        i += 1

Конечно, можно разделить аннотацию функции на несколько строк:

from typing import Union, Optional

def send_email(address: Union[str, list[str]],
               sender: str,
               cc: Optional[list[str]],
               bcc: Optional[list[str]],
               subject: str = '',
               body: Optional[list[str]] = None
               ) -> bool:
    ...

Python понимает аргументы только по позиции и только по ключевым словам. Позиционные аргументы также могут быть помечены с помощью имени, начинающегося с двух подчеркиваний.

def quux(x: int, /, *, y: int) -> None:
    pass

# Ok
quux(3, y=5)
# ошибка: слишком много позиционных аргументов для «quux»
quux(3, 5)
# ошибка: неожиданный ключевой аргумент "x" для "quux"
quux(x=3, y=5)

В следующей аннотации говорится, что каждый позиционный аргумент и каждый ключевой аргумент являются типом str.

def call(self, *args: str, **kwargs: str) -> str:
    # Выявленный тип "tuple[str, ...]"
    reveal_type(args)
    # Выявленный тип "dict[str, str]"
    reveal_type(kwargs)
    request = make_request(*args, **kwargs)
    return self.do_api_query(request)

Аннотация классов в Python

Метод __init__() ничего не возвращает, поэтому он получает возвращаемый тип None, как и любой другой метод, который ничего не возвращает.

class BankAccount:
    def __init__(self, account_name: str, initial_balance: int = 0) -> None:
        # Python определит правильные типы для этих переменных 
        # экземпляра на основе указанных типов аргументов.
        self.account_name = account_name
        self.balance = initial_balance

    # Методы, опускающие тип для `self`
    def deposit(self, amount: int) -> None:
        self.balance += amount

    def withdraw(self, amount: int) -> None:
        self.balance -= amount

Пользовательские классы действительны как типы в аннотациях.

account: BankAccount = BankAccount("Alice", 400)
def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None:
    src.withdraw(amount)
    dst.deposit(amount)

Функции, принимающие BankAccount(), также принимают любой подкласс BankAccount()!

class AuditedBankAccount(BankAccount):
    # При желании можно объявить переменные экземпляра в теле класса.
    audit_log: list[str]

    def __init__(self, account_name: str, initial_balance: int = 0) -> None:
        super().__init__(account_name, initial_balance)
        self.audit_log: list[str] = []

    def deposit(self, amount: int) -> None:
        self.audit_log.append(f"Deposited {amount}")
        self.balance += amount

    def withdraw(self, amount: int) -> None:
        self.audit_log.append(f"Withdrew {amount}")
        self.balance -= amount

audited = AuditedBankAccount("Bob", 300)
# аннотацию `transfer()` смотрите выше
transfer(audited, account, 100)  # проверка типа!

Можно использовать аннотацию typing.ClassVar для объявления переменной класса:

from typing import ClassVar
class Car:
    seats: ClassVar[int] = 4
    passengers: ClassVar[list[str]]

Если для класса нужны динамические атрибуты, то переопределите __setattr__ или __getattr__:

class A:
    # позволит назначить любой `A.x`, если `x` имеет тот же тип, что и `value`
    # (используйте `value: Any`, чтобы разрешить произвольные типы)
    def __setattr__(self, name: str, value: int) -> None: ...

    # позволит получить доступ к любому `A.x`, 
    # если `x` совместим с возвращаемым типом.
    def __getattr__(self, name: str) -> int: ...

# Работает
a.foo = 42
# Не проходит проверка типа
a.bar = 'Ex-parrot'

Что делать когда все сложно и непонятно

Чтобы узнать, какой тип Python выводит для выражения в любом месте вашей программы, оберните его в reveal_type() (Новое в Python 3.11.). Python напечатает сообщение об ошибке с типом. Удалите этот его еще раз перед запуском кода.

from typing import reveal_type

i = 1
# Раскрытый тип: 'builtins.int'
reveal_type(i)  
l = [1, 2]
# Раскрытый тип: 'builtins.list[builtins.int]'
reveal_type(l)

Если происходит инициализация переменной пустым контейнером или None, то придется немного помочь Python, предоставив явную аннотацию типа:

from typing import Optional

x: list[str] = []
x: Optional[str] = None

Если тип чего-либо невозможно определить (динамический), то для него необходимо использовать typing.Any:

from typing import Any

x: Any = mystery_function()
# Python позволит делать с `x` что угодно!
x.whatever() * x["you"] + x("want") - any(x) and all(x) is super  # нет ошибок

Используйте комментарий type: ignore, чтобы подавить ошибки, когда код сбивает с толку Python или сталкивается с явной ошибкой. Хорошей практикой является добавление комментария, объясняющего проблему.

from typing import Union, Any, Optional, TYPE_CHECKING, cast

# комментарий `type: ignore` подавляет ошибки в данной строке
x = confusing_function()  # type: ignore # не вернет `None`, потому что...

typing.cast - это вспомогательная функция, которая позволяет переопределить выводимый тип выражения.

from typing import cast, reveal_type

a = [4]
# Проходит нормально
b = cast(list[int], a)
# Проходит нормально, несмотря на то, что 
# является ложью (без проверки во время выполнения)
c = cast(list[str], a)
# Выявленный тип - "builtins.list[builtins.str]"
reveal_type(c)
# Все еще печатает [4] ... 
# объект не изменяется и не приводится во время выполнения
print(c)

Если необходимо иметь код, который Python может видеть, но не будет выполнен во время выполнения (или иметь код, который Python не может видеть), то необходимо использовать typing.TYPE_CHECKING с оператором if/else:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import json
else:
    import orjson as json  # Python не знает об этом

Стандартная утиная типизация в Python

В типичном коде Python многим функциям, которые могут принимать список или словарь в качестве аргумента, нужно, чтобы их аргумент был каким-то образом "похож на список" или "похож на словарь". Конкретное значение слов "похожий на список" или "похожий на словарь" (или что-то в этом роде) называется "duck types" (утиной типизацией), и несколько типов уток, которые распространены в идиоматическом Python, стандартизированы.

Нужно использовать Iterable для универсальных итераций (все, что можно использовать в for) и Sequence, где требуется последовательность (поддерживающая len() и __getitem__)

from typing import Iterable, Sequence

def f(ints: Iterable[int]) -> list[str]:
    return [str(x) for x in ints]

f(range(1, 3))

Отображение typing.Mapping описывает объект, похожий на словарь dict, который не будет изменятся (с __getitem__), а typing.MutableMapping изменяемый словарь (с __setitem__)

from typing import Mapping, MutableMapping

def f(my_mapping: Mapping[int, str]) -> list[int]:
    # Python будет жаловаться на следующую строку...
    my_mapping[5] = 'maybe'
    return list(my_mapping.keys())

f({3: 'yes', 4: 'no'})


def f(my_mapping: MutableMapping[int, str]) -> set[str]:
    # с этим Python будет согласен
    my_mapping[5] = 'maybe'  
    return set(my_mapping.values())

f({3: 'yes', 4: 'no'})

Используйте IO[str] или IO[bytes] для функций, которые должны принимать или возвращать объекты, полученные из вызова open() (обратите внимание, что IO не различает чтение, запись или другие режимы)

import sys
from typing import IO

def get_sys_IO(mode: str = 'w') -> IO[str]:
    if mode == 'w':
        return sys.stdout
    elif mode == 'r':
        return sys.stdin
    else:
        return sys.stdout

Прямые ссылки

Возможно, будет необходимо сослаться на класс до его определения. Это известно как "прямая ссылка".

# завершится ошибкой во время выполнения, 
# так как класс 'A' не определен
def f(foo: A) -> int: 
    ...

# если добавить следующий специальный импорт:
from __future__ import annotations
# Это будет работать во время выполнения, 
# и проверка типа будет успешной до тех пор, 
# пока позже в файле появиться класс с таким именем
def f(foo: A) -> int:  # Ok
    ...

# Другой вариант - взять тип в кавычки
def f(foo: 'A') -> int:  # Тоже ок
    ...

class A:
    # Такое может произойти, если нужно сослаться на класс 
    # в аннотации типа внутри определения этого класса.
    @classmethod
    def create(cls) -> A:
        ...

Аннотация декораторов в Python

Функции декоратора могут быть выражены через дженерики.

from typing import Any, Callable, TypeVar

F = TypeVar('F', bound=Callable[..., Any])

def bare_decorator(func: F) -> F:
    ...

def decorator_args(url: str) -> Callable[[F], F]:
    ...

Аннотировать декоратор можно при помощи спецификации параметра (typing.ParamSpec):

from typing import Callable, TypeVar
from typing_extensions import ParamSpec

P = ParamSpec('P')
T = TypeVar('T')

def printing_decorator(func: Callable[P, T]) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwds: P.kwargs) -> T:
        print("Calling", func)
        return func(*args, **kwds)
    return wrapper

Спецификации параметров также позволяют описывать декораторы, которые изменяют сигнатуру передаваемой функции:

from typing import Callable, TypeVar, ParamSpec, reveal_type

P = ParamSpec('P')
T = TypeVar('T')

# повторно используем 'P' в возвращаемом типе, 
# но заменяем 'T' на 'str'
def stringify(func: Callable[P, T]) -> Callable[P, str]:
    def wrapper(*args: P.args, **kwds: P.kwargs) -> str:
        return str(func(*args, **kwds))
    return wrapper

 @stringify
def add_forty_two(value: int) -> int:
    return value + 42

a = add_forty_two(3)
# Выявленный тип: 'builtins.str'
reveal_type(a)
# ошибка: аргумент 1 для "add_forty_two" 
# имеет несовместимый тип "str"; ожидаемый "int"
add_forty_two('x')

Фабрики декораторов

Функции, которые принимают аргументы и возвращают декоратор (также называемые декораторами второго порядка), аналогичным образом поддерживаются через дженерики:

from typing import Any, Callable, TypeVar

F = TypeVar('F', bound=Callable[..., Any])

def route(url: str) -> Callable[[F], F]:
    ...

@route(url='/')
def index(request: Any) -> str:
    return 'Hello world'

Иногда один и тот же декоратор поддерживает как простые вызовы, так и вызовы с аргументами. Этого можно достичь, комбинируя с @overload:

from typing import Any, Callable, Optional, TypeVar, overload

F = TypeVar('F', bound=Callable[..., Any])

# Использование простого декоратора
@overload
def atomic(__func: F) -> F: ...

# Декоратор с аргументами
@overload
def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ...

# Реализация
def atomic(__func: Optional[Callable[..., Any]] = None, *, savepoint: bool = True):
    def decorator(func: Callable[..., Any]):
        ...  # Здесь находится код
    if __func is not None:
        return decorator(__func)
    else:
        return decorator

# Использование
@atomic
def func1() -> None: ...

@atomic(savepoint=False)
def func2() -> None: ...

Аннотация сопрограмм и асинхронности

Функции, определенные с помощью async def, типизируются аналогично обычным функциям. Аннотация типа возвращаемого значения должна совпадать с типом значения, которое ожидается получить обратно при ожидании await сопрограммы.

import asyncio

async def format_string(tag: str, count: int) -> str:
    return f'T-minus {count} ({tag})'

async def countdown(tag: str, count: int) -> str:
    while count > 0:
        my_str = await format_string(tag, count)  # тип подразумевается как `str`
        print(my_str)
        await asyncio.sleep(0.1)
        count -= 1
    return "Blastoff!"

asyncio.run(countdown("Millennium Falcon", 5))

Результатом вызова функции async def без ожидания await будет автоматически считаться значением типа typing.Coroutine[Any, Any, T], который является подтипом typing.Awaitable[T]:

from typing import reveal_type
my_coroutine = countdown("Millennium Falcon", 5)
# Выявленный тип: `typing.Coroutine[Any, Any,builtins.str]`
reveal_type(my_coroutine)