Что такое type annotations и typing? Для чего используют Protocol, TypeVar?

Type annotations и модуль typing добавляют в Python статические подсказки типов, а Protocol и TypeVar нужны для более выразительного описания интерфейсов и дженериков поверх этих подсказок.

Подробный ответ

Что такое type annotations и typing

Type annotations (type hints) — это синтаксис вида x: int, def f(x: str) -> int: ..., который говорит: «я ожидаю здесь такой тип», но не меняет поведение кода во время выполнения. Они используются статическими анализаторами (mypy, pyright), IDE и фреймворками (например, FastAPI), чтобы находить ошибки, улучшать автодополнение и генерировать схемы/валидацию.

Модуль typing даёт «словарь типов»: List, Dict, Optional, Union, Callable, Literal, Annotated и т.д., а также продвинутые механизмы вроде Protocol, TypeVar, Generic.

Пример базовых аннотаций:

from typing import List

def average(values: List[float]) -> float:
   return sum(values) / len(values)

Зачем нужен Protocol

Protocol — это способ описать структурный тип, т.е. интерфейс «утка, которая крякает так-то», а не «класс, унаследованный от такого-то базового класса». Класс считается совместимым с протоколом, если имеет нужные методы/атрибуты с подходящими сигнатурами, даже если он не наследуется от Protocol и вообще про него не знает. Это формализует привычный Python‑овский duck typing ("утиная типизация") на уровне статической типизации.

from typing import Protocol

class Notifier(Protocol):
   def send(self, message: str) -> None: ...
   
class EmailNotifier:
   def send(self, message: str) -> None:
       print("Email:", message)
       
class SmsNotifier:
   def send(self, message: str) -> None:
       print("SMS:", message)
       
def notify(user_notifier: Notifier, msg: str) -> None:
   user_notifier.send(msg)

И EmailNotifier, и SmsNotifier автоматически подходят под Notifier, потому что у них есть метод send(str) -> None.

Protocol позволяет описывать интерфейсы в духе duck typing: я говорю, какие методы и атрибуты мне нужны, а тип‑чекер считает любой класс совместимым, если он это реализует, без наследования и общей иерархии.

Зачем нужен TypeVar

TypeVar — это параметр типа, который даёт возможность писать обобщённый (generic) код: функция одна, но типы входа/выхода связаны между собой.

Мы объявляем T = TypeVar("T") и дальше используем T в аннотациях, чтобы сказать: «функция принимает и возвращает один и тот же тип, но какой именно — решается при использовании». Type checker потом подставляет конкретные типы и проверяет, что ты не смешиваешь, например, int и str там, где обещал один и тот же T.

Пример функции, которая «сохраняет» тип:

from typing import TypeVar, Iterable, List

T = TypeVar("T")
def to_list(items: Iterable[T]) -> List[T]:
   return list(items)

Если передать Iterable[int], тип результата будет List[int]. Если Iterable[str]List[str] и т.д.

TypeVar можно ограничивать: T = TypeVar("T", bound=BaseClass) или T = TypeVar("T", int, float) — тогда функция будет работать только с указанными типами, а checker это гарантирует.

Protocol + TypeVar вместе

Protocol и TypeVar часто комбинируют, чтобы описывать дженерик‑интерфейсы:

from typing import Protocol, TypeVar

T = TypeVar("T")
class Repository(Protocol[T]):
   def get(self, id: int) -> T: ...
   def save(self, obj: T) -> None: ...

Так можно описать общий интерфейс репозитория поверх доменных сущностей (User, Order, Product), сохранив корректные типы туда/обратно. IDE и статический анализатор знают, что UserRepository вернёт User, а OrderRepositoryOrder, хотя код интерфейса один и тот же.

Формулировка для ответа

TypeVar — это параметр типа для дженериков: он связывает типы аргументов и результата функции/класса. Protocol описывает интерфейс через набор методов и атрибутов. Вместе они позволяют строить типобезопасные generic‑интерфейсы в духе репозиториев, клиентских обёрток и т.п.

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге