Зачем применять typing при разработке на 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 NonePython 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 typeuser 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.