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

Поддержка аннотации типов в Python

Внимание. Интерпретатор Python не проверяет и не принимает во внимание аннотации типов функций и переменных. Их могут использовать сторонние инструменты, такие как средства проверки типов, IDE, линтеры и т. д.

Модуль typing обеспечивает поддержку выполнения аннотации типов. Наиболее фундаментальная поддержка состоит из типов typing.Any, typing.Union, typing.Tuple, typing.Callable, typing.TypeVar и typing.Generic.

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

def greeting(name: str) -> str:
    return 'Hello ' + name

В функции greeting() ожидается, что имя аргумента будет иметь тип str и возвращаемый тип ожидается str. Подтипы принимаются в качестве аргументов типов. Например переменная списка lst, состоящий из значений int будет аннотироваться как lst: list[int].

Примечание. Модуль typing определяет несколько типов, которые являются подклассами уже существующих классов стандартной библиотеки, и которые также расширяют typing.Generic для поддержки типов переменных внутри [] скобок. С версии Python 3.9, классы стандартной библиотеки были расширены для поддержки синтаксиса [] и эти типы стали избыточными.

Избыточные типы устарели в Python 3.9, но интерпретатор не будет выдавать предупреждений DEPRECATED. Ожидается, что средства проверки типов будут отмечать устаревшие типы, когда проверяемый код нацелен на версию Python 3.9 или новее.

Устаревшие типы будут удалены из модуля typing в первой версии Python, выпущенной через 5 лет после выпуска Python 3.9.0.

Содержание:

Использование псевдонимов типов в модуле typing.

Начиная с версии Python 3.12, псевдоним типа определяется с помощью оператора type (добавлен в Python 3.12), который создает экземпляр TypeAliasType. В этом примере Vector и list[float] будут обрабатываться средствами проверки статического типа одинаково:

# синтаксис ДО Python 3.12
# Vector = list[float]

# синтаксис начиная с Python 3.12
type Vector = list[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

# Тип будет проверяться; 
# список значений `float` квалифицируется как вектор.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

Псевдонимы типов полезны для упрощения сигнатур сложных типов. Например:

from collections.abc import Sequence

# синтаксис ДО Python 3.12
# ConnectionOptions = dict[str, str]
# Address = tuple[str, int]
# Server = tuple[Address, ConnectionOptions]

# синтаксис начиная с Python 3.12
type ConnectionOptions = dict[str, str]
type Address = tuple[str, int]
type Server = tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
    ...

# Средство проверки статического типа будет рассматривать 
# подпись предыдущего типа как точно эквивалентную этой.
def broadcast_message(
        message: str,
        servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None:

Оператор type является новым в Python 3.12.

Для обратной совместимости псевдонимы типов также можно создавать с помощью простого присваивания:

Vector = list[float]

Или пометить typing.TypeAlias, чтобы было ясно, что это псевдоним типа, а не обычное присвоение переменной:

from typing import TypeAlias

Vector: TypeAlias = list[float]

Аннотация отдельных типов typing.NewType.

Используйте вспомогательный класс typing.NewType() для создания отдельных типов:

from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(524313)

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

def get_user_name(user_id: UserId) -> str:
    ...

# Тип будет проверяться
user_a = get_user_name(UserId(42351))

# тип не проверяется; `int` не является UserId
user_b = get_user_name(-1)

По-прежнему можно выполнять все операции с int с переменной типа UserId, но результат всегда будет иметь тип int. Это позволяет передать UserId везде, где можно ожидать int, но предотвратит случайное создание UserId недопустимым способом:

# 'output' имеет тип 'int', а не 'UserId'
output = UserId(23413) + UserId(54341)

Обратите внимание, что эти проверки выполняются только средством проверки типов. Во время выполнения оператор Derived= NewType('Derived', Base) сделает Derived функцией, которая немедленно возвращает любой переданный ей параметр. Это означает, что выражение Derived(some_value) не создает новый класс и не вводит никаких накладных расходов, помимо обычных вызовов функции.

Точнее, выражение some_value is Derived(some_value) всегда истинно во время выполнения.

Это также означает, что невозможно создать подтип Derived, т. к. во время выполнения это функция идентификации, а не фактический тип:

from typing import NewType

UserId = NewType('UserId', int)

# Не работает во время выполнения и не проверяет тип
class AdminUserId(UserId): pass

Однако можно создать typing.NewType() на основе производного NewType:

from typing import NewType

UserId = NewType('UserId', int)

ProUserId = NewType('ProUserId', UserId)

и проверка типов для ProUserId будет работать так, как ожидалось.

Примечание. Напомним, что использование псевдонима типа объявляет, что два типа эквивалентны друг другу. Выполнение Alias ​​= Original заставит средство проверки статического типа обрабатывать псевдоним как полностью эквивалентный оригиналу во всех случаях. Это полезно, когда необходимо упростить сигнатуры сложных типов.

Напротив, typing.NewType объявляет один тип подтипом другого. Выполнение Derived = NewType('Derived', Original) заставит средство проверки аннотации типов рассматривать Derived как подкласс Original, это означает, что значение типа Original не может использоваться в местах, где ожидается значение типа Derived. Такое поведение полезно, когда необходимо предотвратить логические ошибки с минимальными затратами времени выполнения.

Изменено в Python 3.10: NewType теперь является классом, а не функцией. При вызове NewType вместо обычной функции возникают некоторые дополнительные затраты времени выполнения. В Python 3.11.0 эти затраты будут снижены.

Изменено в версии 3.11: Производительность вызова NewType восстановлена ​​до уровня Python 3.9.

Аннотирование вызываемых объектов.

Функции или другие вызываемые объекты можно аннотировать с помощью Collections.abc.Callable или typing.Callable. Callable[[int], str] означает функцию, которая принимает один параметр типа int и возвращает строку str.

Например:

from collections.abc import Callable, Awaitable

def feeder(get_next_item: Callable[[], str]) -> None:
    ...  # тело функции

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    ...  # тело функции

async def on_update(value: str) -> None:
    ...  # тело функции

callback: Callable[[str], Awaitable[None]] = on_update

Синтаксис подписки Callable() всегда должен использоваться ровно с двумя значениями: списком аргументов и типом возвращаемого значения. Список аргументов должен быть списком типов, ParamSpec, Concatenate или многоточием. Возвращаемый тип должен быть одного типа.

Если в качестве списка аргументов указано многоточие ..., это указывает на то, что вызываемый объект с любым произвольным списком параметров был бы приемлемым:

def concat(x: str, y: str) -> str:
    return x + y

x: Callable[..., str]
x = str     # OK
x = concat  # Also OK

Callable() не может выражать сложные сигнатуры, такие как функции, которые принимают переменное количество аргументов, overloaded functions или функции, которые имеют только ключевые аргументы. Однако эти сигнатуры могут быть выражены путем определения класса typing.Protocol с методом __call__():

from collections.abc import Iterable
from typing import Protocol

class Combiner(Protocol):
    def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ...

def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes:
    for item in data:
        ...

def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
    ...
def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
    ...

batch_proc([], good_cb)  # OK
# Ошибка! Аргумент 2 имеет несовместимый тип из-за
# другого имени и вида в обратном вызове
batch_proc([], bad_cb)

Вызываемые объекты, которые принимают другие вызываемые объекты в качестве аргументов, могут указывать на то, что их типы параметров зависят друг от друга с помощью ParamSpec. Кроме того, если этот вызываемый объект добавляет или удаляет аргументы из других вызываемых объектов, то может использоваться оператор typing.Concatenate. Они принимают форму Callable[ParamSpecVariable, ReturnType] и Callable[Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable], ReturnType] соответственно.

Изменено в версии Python 3.10: Callable теперь поддерживает ParamSpec и Concatenate.

Также смотрите документацию для typing.ParamSpec и typing.Concatenate]typing.Concatenate, в которой приведены примеры использования в Callable.

Аннотация универсальных типов.

Так как информация о типах объектов, хранящихся в контейнерах, не может быть статически представлена универсальным способом, для обозначения ожидаемых типов элементов контейнера, были расширены абстрактные базовые классы.

from collections.abc import Mapping, Sequence

class Employee: ...

# `Sequence[Employee]` указывает, что все элементы в последовательности
# должны быть экземплярами `Employee`.
# `Mapping[str, str]` указывает, что все ключи и все значения в сопоставлении
# должны быть строками.
def notify_by_email(employees: Sequence[Employee],
                    overrides: Mapping[str, str]) -> None: ...

Универсальные функции и классы могут быть аннотированы с помощью синтаксиса списка параметров типа:

from collections.abc import Sequence

# Функция является универсальной для TypeVar "T" 
def first[T](l: Sequence[T]) -> T:
    return l[0]

Или с помощью фабрики TypeVar напрямую:

from collections.abc import Sequence
from typing import TypeVar

# Объявляем переменную типа "U"
U = TypeVar('U')

# Функция является универсальной по сравнению с TypeVar "U",
def second(l: Sequence[U]) -> U:
    return l[1]

Определяемые пользователем универсальные типы.

В Python 3.12 пользовательский класс можно определить как универсальный класс.

from logging import Logger

class LoggedVar[T]:
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)

Этот синтаксис указывает, что класс LoggedVar аннотирован вокруг одного type variable T . Это также делает T допустимым типом в теле класса.

Для совместимости с Python < 3.11 также возможно явное наследование от Generic для указания универсального класса:

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)

Generic[T] как базовый класс определяет, что класс LoggedVar принимает единственный параметр типа T. Это также делает T действительным как тип в теле класса.

Базовый класс typing.Generic определяет __class_getitem__(), так что LoggedVar[t] действителен как тип:

from collections.abc import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

Универсальный тип может иметь любое количество переменных типа. Все разновидности TypeVar допустимы в качестве параметров для универсального типа:

from typing import TypeVar, Generic, Sequence

class WeirdTrio[T, B: Sequence[bytes], S: (int, str)]:
    ...

OldT = TypeVar('OldT', contravariant=True)
OldB = TypeVar('OldB', bound=Sequence[bytes], covariant=True)
OldS = TypeVar('OldS', int, str)

class OldWeirdTrio(Generic[OldT, OldB, OldS]):
    ...

Все аргументы переменной типа для typing.Generic должны быть разными. Таким образом, следующее неверно:

from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]):   # INVALID
    ...

Вы можете использовать множественное наследование с typing.Generic:

from collections.abc import Sized
from typing import TypeVar, Generic

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

При наследовании от универсальных классов можно исправить некоторые переменные типа:

from collections.abc import Mapping
from typing import TypeVar

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

В этом случае MyDict имеет единственный параметр T.

Использование универсального класса без указания параметров типа предполагает typing.Any для каждой позиции. В следующем примере MyIterable не является универсальным, а неявно наследуется от Iterable[Any]:

from collections.abc import Iterable

# Такой же как `Iterable[Any]`
class MyIterable(Iterable):

В Python 3.12 поддерживаются определенные пользователем псевдонимы универсального типа. Примеры:

from collections.abc import Iterable

type Response[S] = Iterable[S] | int

# Возвращаемый тип здесь такой же, как Iterable[str] | int
def response(query: str) -> Response[str]:
    ...

type Vec[T] = Iterable[tuple[T, T]]

# То же, что и Iterable[tuple[T, T]]
def inproduct[T: (int, float, complex)](v: Vec[T]) -> T: 
    return sum(x*y for x, y in v)

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

from collections.abc import Iterable
from typing import TypeVar

S = TypeVar("S")
Response = Iterable[S] | int

Изменено в Python 3.7: typing.Generic больше не имеет собственного метакласса.

Изменено в Python 3.12: Синтаксическая поддержка универсальных шаблонов и псевдонимов типов появилась в Python 3.12. Раньше универсальные классы должны были явно наследоваться от Generic или содержать переменную типа в одной из своих баз.

Определяемые пользователем универсальные типы для аннотирования параметров также поддерживаются с помощью переменных спецификации параметров в форме [**P]. Поведение согласуется с описанными выше переменными типа, поскольку переменные спецификации параметров обрабатываются модулем ввода как специализированная переменная типа. Единственным исключением из этого является то, что список типов может быть использован для замены параметра SPEC:

# T - это переменная типа; 
# P - это ParamSpec
>>> class Z[T, **P]: ...  
...
>>> Z[int, [dict, float]]
# __main__.Z[int, [dict, float]]

Классы, универсальные для ParamSpec, также можно создавать с использованием явного наследования от Generic. В этом случае ** не используется:

>>> from typing import Generic, ParamSpec, TypeVar

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

>>> class Z(Generic[T, P]): ...
...
>>> Z[int, [dict, float]]
# __main__.Z[int, (<class 'dict'>, <class 'float'>)]

Еще одно различие между TypeVar и ParamSpec заключается в том, что универсальный вариант только с одной переменной спецификации параметра будет принимать списки параметров в формах X[[Type1, Type2, ...]], а также X[Type1, Type2, ...] по эстетическим соображениям. Внутри последние преобразуются в первые и, таким образом, эквивалентны:

>>> class X[**P]: ...
...
>>> X[int, str]
# __main__.X[[int, str]]
>>> X[[int, str]]
# __main__.X[[int, str]]

Обратите внимание, что универсальные типы с typing.ParamSpec, в некоторых случаях, могут не иметь правильных __parameters__ после подстановки, потому что они предназначены в первую очередь для проверки статического типа.

Изменено в Python 3.10: Generic теперь можно параметризовать с помощью выражений ParamSpec.

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

Применение аннотации typing.Any.

Особый тип аннотации typing.Any. Средство проверки статического типа будет рассматривать каждый тип как совместимый с Any и Any как совместимый с каждым типом.

Это означает, что можно выполнить любую операцию или вызов метода для значения типа Any и присвоить его любой переменной:

from typing import Any

a = None    # type: Any
a = []      # OK
a = 2       # OK

s = ''      # type: str
s = a       # OK

def foo(item: Any) -> int:
    # Проверка типов; 'item' может быть 
    # любого типа, и этот тип может 
    # иметь метод 'bar'
    item.bar()
    ...

Обратите внимание, что при присвоении значения типа Any более точному типу, проверка типов выполняться не будет. Например, средство проверки аннотации не сообщило об ошибке при присвоении a параметру s, даже если s был объявлен как имеющий тип str и получил значение int во время выполнения!

Кроме того, все функции без возвращаемого типа или типов параметров неявно по умолчанию будут использовать Any:

def legacy_parser(text):
    ...
    return data

# Статическая проверка типов будет 
# рассматривать вышеприведенное 
# как имеющее ту же сигнатуру, что и:
def legacy_parser(text: Any) -> Any:
    ...
    return data

Такое поведение позволяет использовать typing.Any в качестве аварийного выхода, когда необходимо смешивать динамически и статически типизированный код.

Сравните поведение typing.Any с поведением object: Подобно Any, каждый тип является подтипом object. Однако, в отличие от Any, обратное неверно: object не является подтипом любого другого типа.

Это означает, что, когда типом значения является объект, то средство проверки типов отклоняет почти все операции с ним, и присвоение его переменной (или использование в качестве возвращаемого значения) более специализированного типа является ошибкой типа. Например:

def hash_a(item: object) -> int:
    # Не проходит; у `object` нет `magic` метода.
    item.magic()
    ...

def hash_b(item: Any) -> int:
    # Проверка типа прошла успешно
    item.magic()
    ...

# Проверка прошла, поскольку `ints` и 
# `strs` являются подклассами объекта
hash_a(42)
hash_a("foo")

# Проверка прошла, т.к. `Any` 
# совместим со всеми типами
hash_b(42)
hash_b("foo")

Используйте object, для указания значения любого типа безопасным способом, а typing.Any, для динамически типизируемых значений.

Номинальные и структурные подтипы.

Первоначально определи систему статических типов Python, как использование номинальных подтипов. Это означает, что класс A разрешен там, где ожидается класс B тогда и только тогда, когда A является подклассом B.

Это требование ранее также применялось к абстрактным базовым классам, таким как Iterable. Проблема с этим подходом заключается в том, что класс должен быть явно помечен для их поддержки, что не является питоническим и отличается от того, что обычно делают в идиоматическом динамически типизированном коде Python. Например:

from collections.abc import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

Однако пользователи могут писать приведенный выше код без явных базовых классов в определении класса, что позволяет средствам проверки статических типов неявно рассматривать Bucket как подтип Sized и Iterable[int]. Это называется структурным подтипом или статической утиной типизацией:

from collections.abc import Iterator, Iterable

# Примечание: нет базовых классов
class Bucket:  
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result = collect(Bucket())  # Проходит проверку типа

Более того, создавая подклассы для специального класса Protocol, пользователь может определять новые настраиваемые протоколы, чтобы в полной мере насладиться структурным подтипом.