Зачем применять typing при разработке на Python

Раздел: Основы Python -> Аннотации типов

Базовые конструкции для описания типов

Аннотации типов помогают передать намерение разработчика и дают возможность статическому анализатору находить ошибки до запуска. Модуль typing предлагает набор инструментов для таких аннотаций. В этом разделе основное внимание уделено практическим вариантам записи типов.

Как правильно аннотировать функцию, чтобы проверка проходила без ошибок?

Основной вариант состоит в использовании typing вместе с внешним анализатором mypy. Сначала в коде появляются аннотации, затем выполняется проверка. Аннотации не влияют на работу интерпретатора.

from typing import List, Optional

def find_users(users: List[dict], role: str) -> Optional[dict]:
    for user in users:
        if user['role'] == role:
            return user
    return None

Python typing (модуль typing в python)

List[dict] описывает список словарей. Optional[dict] говорит о том, что функция может вернуть None. Для более точного описания словаря используется Dict[str, str], но в простых случаях подходит и dict.

mypy example.py

Анализатор завершится без сообщений, если аннотации соответствуют коду. При несоответствии mypy укажет строку и ожидаемый тип.

Какие проблемы возникают при слишком общих аннотациях?

Использование dict без параметров допустимо, но не позволяет проверить типы ключей и значений. Аналогичная ситуация с List. Если внутри списка ожидаются числа, а попадёт строка, mypy пропустит ошибку. Для точного описания нужны List[int], Dict[str, str], Tuple[int, str]. Ещё одна частая проблема: Optional[dict] не определяет содержимое словаря, поэтому внутри словаря может оказаться что угодно.

Какой вариант использовать для объединения нескольких типов?

Union позволяет указать несколько допустимых типов. Optional является частным случаем Union, где один из типов это None. Union удобен для функций, которые принимают разные виды данных.

from typing import Union

def encode(value: Union[str, int]) -> str:
    return str(value)

print(encode('text'))
print(encode(12))
text
12

Этот вариант применяется, когда тип аргумента заранее не фиксирован, но допустимый набор ограничен.

Как описать функцию обратного вызова?

Callable указывает сигнатуру функции для передачи в другую функцию. В квадратных скобках перечисляются типы аргументов и тип возвращаемого значения.

from typing import Callable

def map_value(value: int, func: Callable[[int], str]) -> str:
    return func(value)

def to_label(value: int) -> str:
    return f'#{value}'

print(map_value(3, to_label))
#3

Callable[[int], str] означает функцию с одним аргументом int и результатом str.

Как сохранить обобщённость без потери типа?

TypeVar создаёт параметр типа. Он полезен для функций, которые должны возвращать элемент той же коллекции, не изменяя его тип.

from typing import TypeVar, Sequence

T = TypeVar('T')

def get_first(items: Sequence[T]) -> T:
    return items[0]

text = get_first(['a', 'b'])
number = get_first([1, 2])

Для списка строк T становится str, для списка чисел int. Такой подход точнее, чем использование Any.

Как ограничить возможные значения строки?

Literal ограничивает значение конкретными строками или числами. Это удобно для режимов работы и конфигураций.

from typing import Literal

def set_level(level: Literal['low', 'medium', 'high']) -> None:
    print(level)

set_level('medium')
medium

Значение 'low' или 'high' тоже допустимо. Любая другая строка вызовет замечание анализатора.

Как описать словарь с фиксированными полями?

TypedDict задаёт структуру словаря. Он применяется для объектов, которые проще передавать как dict, но необходимо контролировать ключи.

from typing import TypedDict

class Product(TypedDict):
    name: str
    price: int

def show_product(product: Product) -> str:
    return product['name'] + ': ' + str(product['price'])

data = {'name': 'book', 'price': 500}
print(show_product(data))
book: 500

Как создать небольшую неизменяемую структуру?

NamedTuple даёт класс с атрибутами. Такой вариант удобен для координат, точек и других коротких наборов значений.

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(10, 20)
print(p.x + p.y)
30

Атрибуты доступны по именам, а сам объект остаётся неизменяемым.

Какие ошибки часто возникают при импорте типов?

В старых версиях Python отсутствуют некоторые классы typing, например TypeAlias или Annotated. Для совместимости стоит проверять версию интерпретатора. Другая ошибка связана с использованием list вместо List. Начиная с Python 3.9 такое написание разрешено, но для кода, работающего на Python 3.8, лучше подходит typing.List. Также нельзя смешивать typing.List и list в одном проекте без установки правила для анализатора.

Практические примеры с typing

В этой части перечислены дополнительные сценарии применения typing. Каждый пример содержит код и результат его выполнения.

Пример 1. TypeVar с ограничением

Пример
from typing import TypeVar

Number = TypeVar('Number', int, float)

def square(value: Number) -> Number:
    return value * value

print(square(3))
print(square(2.5))
9
6.25

TypeVar с двумя ограничениями разрешает только int и float. Функция возвращает значение того же типа. Если передать строку, mypy сообщит об ошибке.

Пример 2. Перегрузка функций

Пример
from typing import overload, Union

@overload
def convert(value: str) -> int: ...

@overload
def convert(value: int) -> str: ...

def convert(value: Union[str, int]) -> Union[str, int]:
    if isinstance(value, str):
        return int(value)
    return str(value)

print(convert('42'))
print(convert(42))
42
42

Для статического анализатора overload определяет точное соответствие между входом и выходом. Реализация остаётся одной функцией.

Пример 3. Протокол и структурная совместимость

Пример
from typing import Protocol

class Named(Protocol):
    name: str

def greeting(obj: Named) -> str:
    return 'Hello, ' + obj.name

class Person:
    def __init__(self, name: str) -> None:
        self.name = name

class Robot:
    def __init__(self, model: str) -> None:
        self.model = model

    @property
    def name(self) -> str:
        return self.model

print(greeting(Person('Alice')))
print(greeting(Robot('R2D2')))
Hello, Alice
Hello, R2D2

Protocol не требует явного наследования. Достаточно совпадения атрибутов.

Пример 4. Annotated с метаданными

Пример
from typing import Annotated

def connect(host: Annotated[str, 'ip or domain name']) -> None:
    print(host)

connect('example.com')
example.com

Annotated добавляет дополнительную информацию о типе. Она не участвует в проверке, но доступна через __metadata__.

Пример 5. TypedDict с необязательными полями

Пример
from typing import TypedDict

class Settings(TypedDict, total=False):
    width: int
    height: int
    title: str

def apply_settings(settings: Settings) -> int:
    return settings.get('width', 0) * settings.get('height', 0)

print(apply_settings({'width': 3, 'height': 4}))
print(apply_settings({}))
12
0

total=False означает, что ключи можно опускать. Указание других ключей не допускается.

Пример 6. NewType для разграничения смысловых типов

Пример
from typing import NewType

UserId = NewType('UserId', int)
ProductId = NewType('ProductId', int)

def find_user(user_id: UserId) -> str:
    return 'user ' + str(user_id)

user = UserId(7)
print(find_user(user))
product = ProductId(3)
# print(find_user(product))  # mypy: incompatible type
user 7

NewType создаёт подтип int. Анализатор не позволяет передать ProductId в аргумент UserId, хотя во время выполнения это обычное число.

Пример 7. get_type_hints для чтения аннотаций

Пример
from typing import get_type_hints, List, Optional

def process(items: List[Optional[int]]) -> Optional[int]:
    return items[0] if items else None

print(get_type_hints(process))
{'items': typing.List[typing.Optional[int]], 'return': typing.Optional[int]}

get_type_hints возвращает словарь с аннотациями. Это удобно для библиотек, которые анализируют сигнатуры.

Пример 8. Literal и Final

Пример
from typing import Final, Literal

MAX_ATTEMPTS: Final = 3

def retry(count: Literal[1, 2, 3]) -> str:
    return 'attempts: ' + str(count)

print(retry(2))
attempts: 2

Literal разрешает только перечисленные значения. Final запрещает повторное присваивание при проверке.

Пример 9. Generic класс

Пример
from typing import Generic, TypeVar, List

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

stack = Stack[int]()
stack.push(1)
print(stack.pop())
1

Generic класс позволяет переиспользовать логику для разных типов. При создании Stack[int] параметр T замещается int.

Модуль typing в Python - comments

En
Python typing (python)