Python 3 typing: аннотации и проверка типов на практике

Раздел: Типы данных -> Модуль 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' не входит в Literal

Python 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, а не int

NewType создаёт псевдоним, который воспринимается анализатором как новый тип, хотя во время выполнения это просто число.

Осторожно:

В 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.value
1
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)  # 16
16

Примечание:

Тип 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))  # Привет от наследника
Привет
Привет от наследника

Модуль typing в Python 3 - comments

En
Python 3 typing (python)