Python 3 typing: аннотации и проверка типов на практике
Основы модуля typing: зачем и как
Модуль typing появился в Python 3.5 и позволяет добавлять аннотации типов к переменным, аргументам функций и возвращаемым значениям. Эти аннотации не влияют на выполнение кода, но служат документацией и используются статическими анализаторами (mypy, Pyre, Pyright) для поиска ошибок на этапе разработки.
Наиболее эффективный способ использования typing - строго аннотировать все публичные функции и методы. Это повышает читаемость, облегчает рефакторинг и позволяет отлавливать несоответствия типов до запуска.
from typing import List, Optional
def process_items(items: List[int], multiplier: Optional[float] = None) -> List[float]:
if multiplier is None:
multiplier = 1.0
return [item * multiplier for item in items]
result = process_items([1, 2, 3], 2.5)
print(result) # [2.5, 5.0, 7.5]Typing object python (typing.object в python)
В коде выше указано, что items - список целых чисел, multiplier - опциональное число с плавающей точкой, а возвращается список чисел с плавающей точкой. Если передать строку или список строк, mypy выдаст предупреждение.
Частая проблема:
Забыть импортировать нужные типы или перепутать регистр (например, List[int] вместо list[int] в Python 3.9+). В Python 3.9+ можно использовать встроенные list[int], но для обратной совместимости часто применяют typing.
Как создать обобщенную функцию, работающую с разными типами?
Используйте TypeVar для объявления параметра типа.
from typing import TypeVar, List
T = TypeVar('T')
def first_element(lst: List[T]) -> T:
return lst[0]
print(first_element([1, 2, 3])) # 1
print(first_element(['a', 'b'])) # 'a'Python typing string (typing.string в python)
Тип T подставляется автоматически: для списка чисел - int, для строк - str.
Ошибка:
Не указывать ограничения (bound) когда нужно разрешить только подмножество типов. Например, если требуется только числовые типы, следует написать T = TypeVar('T', int, float).
Как описать интерфейс без наследования, используя утиную типизацию?
Примените Protocol из модуля typing. Протокол определяет, какие методы должен иметь объект.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw()
class Circle:
def draw(self) -> None:
print("Рисуем круг")
class Square:
def draw(self) -> None:
print("Рисуем квадрат")
render(Circle()) # Рисуем круг
render(Square()) # Рисуем квадрат
Python 3 typing (модуль typing в python 3)
Здесь Drawable - протокол. Любой объект с методом draw будет принят функцией render.
Типичная ошибка:
Не помечать протокол как @runtime_checkable если требуется проверка во время выполнения. По умолчанию протоколы не поддерживают isinstance.
Как ограничить функцию только конкретными строковыми литералами?
Воспользуйтесь Literal.
from typing import Literal
def set_mode(mode: Literal['read', 'write', 'append']) -> None:
print(f"Установлен режим {mode}")
set_mode('read') # OK
set_mode('delete') # mypy: ошибка, 'delete' не входит в LiteralPython typing class (типизация классов в python)
Literal принимает только заданные значения. Это удобно для строковых перечислений.
Проблема:
В Python 3.8 и старше Literal отсутствует - нужно обновить версию или использовать typing_extensions.
Как строго описать структуру словаря с известными ключами и типами значений?
Примените TypedDict.
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
person: Person = {'name': 'Иван', 'age': 30}
# person['salary'] = 100000 # mypy: ошибка, ключ не объявленTypedDict позволяет задать точную схему для словаря. В Python 3.8+ доступен из модуля typing, в более старых - из typing_extensions.
Распространённая ошибка:
Забыть, что TypedDict по умолчанию допускает дополнительные ключи. Чтобы запретить, используйте total=False или укажите TypedDict(..., total=True).
Как создать новый тип, отличный от существующего, чтобы избежать путаницы?
Используйте NewType.
from typing import NewType
UserId = NewType('UserId', int)
def get_user_name(uid: UserId) -> str:
return f"User_{uid}"
uid = UserId(42)
print(get_user_name(uid)) # User_42
# get_user_name(42) # mypy: ошибка, ожидается UserId, а не intNewType создаёт псевдоним, который воспринимается анализатором как новый тип, хотя во время выполнения это просто число.
Осторожно:
В runtime isinstance(uid, int) вернёт True, поэтому не стоит полагаться на защиту во время выполнения. NewType - это подсказка для статического анализатора.
Расширенные примеры работы с typing
Ниже приведены детальные примеры с кодом и выводом, демонстрирующие различные возможности модуля typing в действии.
1. Комбинация Union, Optional и TypeVar
from typing import Union, Optional, TypeVar
Number = Union[int, float]
T = TypeVar('T', int, float)
def add_one(value: T) -> T:
return value + 1 # type: ignore[operator]
def safe_divide(a: Number, b: Optional[Number] = None) -> Optional[float]:
if b is None:
return None
try:
return a / b
except ZeroDivisionError:
return None
print(add_one(5)) # 6
print(add_one(3.2)) # 4.2
print(safe_divide(10, 3)) # 3.333...6 4.2 3.3333333333333335
2. Callable для аннотации функций обратного вызова
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def square(x: int) -> int:
return x * x
result = apply_twice(square, 2)
print(result) # 16 (сначала 4, потом 16)16
3. Iterator и Generator для итераторов
from typing import Iterator, Generator
def count_up_to(n: int) -> Iterator[int]:
i = 1
while i <= n:
yield i
i += 1
for num in count_up_to(3):
print(num)
# Генератор с возвратом значения (Generator[YieldType, SendType, ReturnType])
def range_with_sum(n: int) -> Generator[int, None, int]:
total = 0
for i in range(n):
yield i
total += i
return total
gen = range_with_sum(4)
list(gen) # [0, 1, 2, 3]
# Получить return нельзя через next, только через StopIteration.value1 2 3
4. TypedDict с optional полями и total=False
from typing import TypedDict
class Book(TypedDict, total=False):
title: str
author: str
year: int
book1: Book = {'title': 'Война и мир', 'author': 'Толстой'} # OK
book2: Book = {'title': '1984'} # OK, даже без обязательных полей
# book3: Book = {'year': 1949, 'pages': 328} # mypy: 'pages' не объявлен
print(book1['title']) # Война и мирВойна и мир
5. Использование Any когда тип неизвестен
from typing import Any
def log_and_return(value: Any) -> Any:
print(f"Значение: {value}")
return value
result = log_and_return([1, 2, 3])
# mypy не проверит, что с result можно делать что угодно, но это снижает контроль
print(result[0]) # 1Значение: [1, 2, 3] 1
6. Self для методов, возвращающих экземпляр того же класса
from typing import Self
class Builder:
def __init__(self, value: int = 0) -> None:
self.value = value
def add(self, x: int) -> Self:
self.value += x
return self
def multiply(self, y: int) -> Self:
self.value *= y
return self
builder = Builder(5).add(3).multiply(2)
print(builder.value) # 1616
Примечание:
Тип Self доступен с Python 3.11. В более ранних версиях приходилось возвращать 'Builder' как строку.
7. Type для передачи класса как аргумента
from typing import Type
class Base:
def greet(self) -> str:
return "Привет"
class Child(Base):
def greet(self) -> str:
return "Привет от наследника"
def create_and_greet(cls: Type[Base]) -> str:
instance = cls()
return instance.greet()
print(create_and_greet(Base)) # Привет
print(create_and_greet(Child)) # Привет от наследникаПривет Привет от наследника