Аннотации типов и typing в Python
Python typing на примерах: аннотации переменных и функций, list[int], Optional, TypedDict, Protocol, дженерики, оператор type и проверка кода в mypy.
Содержание · 13 разделов
- Зачем нужны аннотации типов
- Синтаксис аннотаций
- Переменные
- Параметры и результат функции
- Атрибуты классов
- Коллекции: list[int], dict[str, int], tuple
- Старый и новый синтаксис
- Несколько типов и None: Union и Optional
- Модуль typing: основные конструкции
- Any и object
- Literal и Final
- TypedDict: словарь с известными ключами
- Protocol: утиная типизация
- Self и @override
- Annotated: метаданные для библиотек
- NewType: разные типы на основе одного
- TypeIs: сужение типа своей функцией
- Псевдонимы типов: оператор type
- type[User] и оператор type
- Дженерики: TypeVar и синтаксис Python 3.12
- Отложенные аннотации в Python 3.14
- Что нового в typing в Python 3.15
- Как проверить типы: mypy, pyright, ty
- Аннотации во время выполнения
- Частые вопросы
- Ускоряют или замедляют аннотации программу?
- Нужно ли импортировать List, Dict и Optional из typing?
- Как аннотировать функцию, которая ничего не возвращает?
- Аннотации загромождают код. Можно вынести их отдельно?
- Чем TypedDict отличается от dataclass?
- Какой анализатор выбрать?
- Итог
Аннотации типов в Python — подсказки, какие значения ожидаются в переменной, параметре функции и результате: name: str, -> int. Простые типы (int, str, list[int], int | None) пишутся без импорта, а модуль typing добавляет конструкции для сложных случаев: TypedDict, Protocol, Literal, Self, Annotated. Сам интерпретатор аннотации не проверяет — это делают анализаторы вроде mypy и редактор кода:
def greet(name: str, times: int = 1) -> str:
return ", ".join([f"Привет, {name}"] * times)
print(greet("Анна", 2)) # → Привет, Анна, Привет, Анна
print(greet(42)) # → Привет, 42 — Python не возражает
$ mypy main.py
main.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
Found 1 error in 1 file (checked 1 source file)
Ниже — синтаксис аннотаций, основные инструменты typing, дженерики, оператор type и изменения в Python 3.12–3.15. Примеры проверены на Python 3.14 и mypy 2.4, примеры для 3.15 — на Python 3.15. Базовый синтаксис языка собран в шпаргалке по синтаксису Python.
Зачем нужны аннотации типов
Python — язык с динамической типизацией: тип есть у значения, а переменная может ссылаться на что угодно. Ошибку вида «передали строку вместо числа» вы увидите только при запуске, и то если код дойдёт до проблемной строки. Аннотации переносят часть таких ошибок на этап до запуска.
Что они дают на практике:
- Проверку до запуска. mypy, pyright или ty читают аннотации и находят несовместимые типы, забытую проверку на
None, опечатки в атрибутах. - Подсказки в редакторе. VS Code и PyCharm по аннотациям показывают методы объекта и подсвечивают ошибки прямо при наборе.
- Документацию в коде. Сигнатура
def send(to: str, retries: int = 3) -> boolобъясняет функцию лучше комментария и не устаревает незаметно. - Работу библиотек. Pydantic проверяет данные по аннотациям полей, FastAPI строит из них валидацию запросов и документацию API,
dataclassesгенерирует__init__.
Аннотации необязательны: можно добавлять их постепенно, начиная с публичных функций. Неаннотированный код mypy по умолчанию не проверяет, поэтому старый проект не «покраснеет» целиком после первого запуска.
Синтаксис аннотаций
Переменные
Тип пишется после имени через двоеточие. Значение можно не присваивать — тогда это только объявление:
count: int = 0
names: list[str] = []
user_id: int # объявлена, но не создана
print(count, names) # → 0 []
Аннотация переменной не создаёт её: обращение к user_id до присваивания вызовет NameError. Подробнее о самих переменных и встроенных типах — в статье о переменных и типах данных.
Параметры и результат функции
Тип параметра — после двоеточия, тип результата — после стрелки ->. Значение по умолчанию пишется после аннотации:
def average(values: list[float], digits: int = 2) -> float:
return round(sum(values) / len(values), digits)
def log(message: str) -> None: # функция ничего не возвращает
print(message)
Для *args и **kwargs указывают тип одного элемента: *args: int значит «все позиционные аргументы — числа», **kwargs: str — «все именованные аргументы — строки». Основы объявления функций — в статье о функциях в Python.
Атрибуты классов
Атрибуты экземпляра аннотируют в __init__ или в теле класса. Аннотации в теле класса читает @dataclass и строит по ним конструктор:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int = 0
print(User("Анна", 30)) # → User(name='Анна', age=30)
Как устроены классы и методы — в статье о классах в Python.
Коллекции: list[int], dict[str, int], tuple
С Python 3.9 встроенные коллекции принимают тип элементов в квадратных скобках, импортировать List и Dict из typing больше не нужно:
scores: dict[str, int] = {"Анна": 5, "Борис": 4}
point: tuple[float, float] = (1.0, 2.5) # ровно два элемента
row: tuple[int, ...] = (1, 2, 3) # любое число int
tags: set[str] = {"python", "typing"}
matrix: list[list[int]] = [[1, 2], [3, 4]]
У tuple каждый элемент описывается отдельно, а многоточие ... означает «сколько угодно элементов этого типа». Для словаря первым идёт тип ключа, вторым — тип значения. Методы самих коллекций разобраны в статье о словарях в Python.
Если функция только читает данные, аннотируйте параметр абстрактным типом из collections.abc: Iterable[int] примет и список, и кортеж, и генератор, а Sequence[str] — любую последовательность с индексами. Для результата, наоборот, указывайте конкретный тип: так вызывающий код знает, какие методы ему доступны.
from collections.abc import Callable, Iterable
def apply_all(func: Callable[[int], int], values: Iterable[int]) -> list[int]:
return [func(v) for v in values]
print(apply_all(lambda x: x * 2, range(4))) # → [0, 2, 4, 6]
Callable[[int], int] — функция, которая принимает один int и возвращает int. Если параметры неважны, пишут Callable[..., int].
Старый и новый синтаксис
В коде и статьях до 2021 года встречаются импорты из typing. Они по-прежнему работают (на Python 3.15 — без предупреждений), но в новом коде используйте правую колонку:
Было (typing) | Стало | С версии Python |
|---|---|---|
List[int], Dict[str, int] | list[int], dict[str, int] | 3.9 |
Tuple[int, str], Set[str] | tuple[int, str], set[str] | 3.9 |
typing.Callable, typing.Iterable | collections.abc.Callable, collections.abc.Iterable | 3.9 |
Union[int, str] | int | str | 3.10 |
Optional[str] | str | None | 3.10 |
Vector: TypeAlias = list[float] | type Vector = list[float] | 3.12 |
T = TypeVar("T") + Generic[T] | class Box[T], def first[T] | 3.12 |
Несколько типов и None: Union и Optional
Вертикальная черта | объединяет типы: int | str — «число или строка». Самый частый случай — значение, которого может не быть:
def find_user(user_id: int) -> str | None:
users = {1: "Анна", 2: "Борис"}
return users.get(user_id)
name = find_user(3)
print(name.upper())
При запуске код упадёт с AttributeError: 'NoneType' object has no attribute 'upper', а mypy найдёт проблему заранее:
main.py:6: error: Item "None" of "str | None" has no attribute "upper" [union-attr]
После проверки if name is not None: анализатор сужает тип до str, и ошибка исчезает. Optional[str] из typing означает то же, что str | None; запись через | короче и не требует импорта.
Важно:
Optionalописывает только тип значения, обязательность аргумента задаёт значение по умолчанию. Параметрretries: int = 3необязателен при вызове, ноNoneв нём недопустим. ЕслиNoneразрешён, пишитеtimeout: float | None = None.
Модуль typing: основные конструкции
| Конструкция | Что означает | С версии |
|---|---|---|
Any | любой тип, проверка отключается | 3.5 |
Literal["r", "w"] | только перечисленные значения | 3.8 |
Final | константа, переприсваивать нельзя | 3.8 |
TypedDict | словарь с фиксированными ключами и их типами | 3.8 |
Protocol | любой объект с нужными методами («утиная типизация») | 3.8 |
Annotated[int, ...] | тип плюс метаданные для библиотек | 3.9 |
NewType | отдельный тип на основе существующего (UserId из int) | 3.5 |
Self | экземпляр текущего класса | 3.11 |
Never, NoReturn | функция никогда не возвращает управление | 3.11, 3.5 |
override | метод обязан переопределять метод родителя | 3.12 |
TypeIs | функция-проверка, сужающая тип | 3.13 |
TypeForm | значение, которое само является выражением типа | 3.15 |
Если нужной конструкции нет в вашей версии Python, её обычно можно импортировать из пакета typing_extensions: он переносит новые объекты typing на старые версии. Новый синтаксис (class Box[T], оператор type) так не перенести — его разбирает парсер.
Any и object
Any совместим с любым типом в обе стороны и отключает проверку: с ним можно вызвать любой метод. object тоже принимает любое значение, но разрешает только то, что есть у всех объектов:
from typing import Any
def log(value: Any) -> None:
value.anything() # mypy молчит
def log2(value: object) -> None:
value.anything() # error: "object" has no attribute "anything"
Используйте object, когда функции действительно всё равно, что пришло, и Any — как временную заглушку там, где тип пока не описан.
Literal и Final
Literal ограничивает значение конкретным набором, Final запрещает переприсваивание:
from typing import Final, Literal
MAX_RETRIES: Final = 3
def open_file(path: str, mode: Literal["r", "w"] = "r") -> None:
print(f"Открываю {path} в режиме {mode}")
open_file("data.txt", "x")
MAX_RETRIES = 5
main.py:8: error: Argument 2 to "open_file" has incompatible type "Literal['x']"; expected "Literal['r', 'w']" [arg-type]
main.py:9: error: Cannot assign to final name "MAX_RETRIES" [misc]
Код при этом выполнится без ошибок — обе проверки существуют только для анализатора.
TypedDict: словарь с известными ключами
Когда данные приходят как словарь (JSON из API, конфигурация), TypedDict описывает его ключи. В рантайме это обычный dict, никаких накладных расходов:
from typing import NotRequired, TypedDict
class Movie(TypedDict):
title: str
year: int
rating: NotRequired[float] # ключа может не быть
movie: Movie = {"title": "Солярис", "year": 1972}
print(movie["title"], type(movie).__name__) # → Солярис dict
bad: Movie = {"title": "Сталкер", "year": "1979"}
main.py:11: error: Incompatible types (expression has type "str", TypedDict item "year" has type "int") [typeddict-item]
NotRequired (Python 3.11) помечает необязательный ключ. Это отличается от rating: float | None: там ключ обязан быть, но значение может быть None. Если словарь создаёте вы сами, удобнее @dataclass или модель Pydantic — она ещё и проверит данные при выполнении.
Protocol: утиная типизация
Protocol описывает требования к объекту без наследования. Подходит любой класс, у которого есть нужные методы:
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None:
print("Файл закрыт")
class Connection:
def close(self) -> None:
print("Соединение закрыто")
def shutdown(resources: list[SupportsClose]) -> None:
for resource in resources:
resource.close()
shutdown([File(), Connection()])
shutdown([42]) # error: List item 0 has incompatible type "int"; expected "SupportsClose"
File и Connection не наследуют SupportsClose, но подходят, потому что у них есть close(). Так типизируют код, который принимает «что-то с методом read()» или «что-то, что можно сравнить».
Self и @override
Self (3.11) означает «экземпляр того класса, у которого вызван метод», в том числе наследника. @override (3.12) просит анализатор убедиться, что метод с таким именем есть у родителя, и ловит опечатки при переименовании:
from typing import Self, override
class Query:
def __init__(self) -> None:
self.filters: list[str] = []
def where(self, condition: str) -> Self:
self.filters.append(condition)
return self
class UserQuery(Query):
def active(self) -> Self:
return self.where("is_active")
@override
def where(self, condition: str) -> Self:
return super().where(f"user.{condition}")
q = UserQuery().where("age > 18").active()
print(type(q).__name__, q.filters)
# → UserQuery ['user.age > 18', 'user.is_active']
Без Self метод where пришлось бы аннотировать как -> Query, и после UserQuery().where(...) анализатор «забыл» бы про метод active().
Annotated: метаданные для библиотек
Annotated[тип, метаданные] для анализатора равен первому аргументу, а остальное читают библиотеки. На этом построены параметры FastAPI (Annotated[str, Query(max_length=50)]) и ограничения полей Pydantic:
from typing import Annotated, get_type_hints
def set_age(age: Annotated[int, "от 0 до 150"]) -> None: ...
print(get_type_hints(set_age))
# → {'age': <class 'int'>, 'return': <class 'NoneType'>}
print(get_type_hints(set_age, include_extras=True))
# → {'age': typing.Annotated[int, 'от 0 до 150'], 'return': <class 'NoneType'>}
NewType: разные типы на основе одного
NewType создаёт отдельный тип для анализатора, оставляя в рантайме исходный. Так UserId и OrderId нельзя перепутать, хотя оба — int:
from typing import NewType
UserId = NewType("UserId", int)
def get_user(user_id: UserId) -> str:
return f"user-{user_id}"
print(get_user(UserId(5))) # → user-5
get_user(5) # error: Argument 1 to "get_user" has incompatible type "int"; expected "UserId"
TypeIs: сужение типа своей функцией
isinstance() анализатор понимает сам. Если проверка вынесена в отдельную функцию, укажите результат TypeIs[тип] (3.13), и тип сузится в обеих ветках условия:
from typing import TypeIs
def is_str(value: object) -> TypeIs[str]:
return isinstance(value, str)
def describe(value: int | str) -> str:
if is_str(value):
return value.upper() # здесь value: str
return str(value + 1) # здесь value: int
print(describe("ok"), describe(41)) # → OK 42
Предшественник TypeGuard (3.10) сужает тип только в ветке if. Для проверок вида «это строка?» выбирайте TypeIs.
Псевдонимы типов: оператор type
Длинный тип удобно назвать. С Python 3.12 для этого есть оператор type:
type UserId = int
type Json = dict[str, Json] | list[Json] | str | int | float | bool | None
type Pair[T] = tuple[T, T]
def load(user_id: UserId) -> Json:
return {"id": user_id, "tags": ["admin"]}
print(load(7)) # → {'id': 7, 'tags': ['admin']}
print(type(Json).__name__) # → TypeAliasType
print(Json.__value__)
# → dict[str, Json] | list[Json] | str | int | float | bool | None
Правая часть вычисляется лениво, поэтому Json ссылается сам на себя без кавычек. Псевдоним может быть обобщённым (Pair[T]). До 3.12 псевдонимы писали как обычное присваивание Vector = list[float] или с пометкой Vector: TypeAlias = list[float]; TypeAlias объявлен устаревшим, но удалять его не планируют.
Оператор type создаёт объект TypeAliasType, и проверка isinstance({}, Json) выбросит TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union. Если тип нужен для проверок при выполнении, оставьте обычное присваивание.
type[User] и оператор type
Слово type в аннотациях встречается в двух ролях:
type X = ...— оператор Python 3.12+, создаёт псевдоним типа;type[User]в аннотации — сам классUserили его наследник: функция получает класс и может создать из него объект.
def make(cls: type[Exception], msg: str) -> Exception:
return cls(msg)
print(repr(make(ValueError, "плохо"))) # → ValueError('плохо')
Старое написание typing.Type[User] означает то же самое.
Дженерики: TypeVar и синтаксис Python 3.12
Обобщённая функция работает с разными типами, сохраняя связь между ними: first() от списка строк возвращает строку, от списка чисел — число. С Python 3.12 (PEP 695) параметр типа пишется в квадратных скобках после имени:
def first[T](items: list[T]) -> T:
return items[0]
class Stack[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()
numbers = Stack[int]()
numbers.push(10)
print(first(["a", "b"]), numbers.pop()) # → a 10
numbers.push("десять")
main.py:17: error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int" [arg-type]
Ограничения задаются через двоеточие:
def biggest[T: (int, float)](a: T, b: T) -> T: # T — только int или float
return a if a > b else b
def longest[S: str](items: list[S]) -> S: # S — str или его подкласс
return max(items, key=len)
print(biggest(3, 7), longest(["кот", "собака"])) # → 7 собака
С Python 3.13 у параметра типа может быть значение по умолчанию: class Box[T = int]. Голое Box в аннотации тогда означает Box[int].
На Python 3.11 и более ранних версиях новый синтаксис вызывает SyntaxError. Если проект должен работать там, используйте прежнюю запись — mypy понимает обе:
from typing import Generic, TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
Смешивать стили в одном классе нельзя: class A[U](Generic[T]) вызывает TypeError.
Отложенные аннотации в Python 3.14
До Python 3.14 аннотации вычислялись в момент определения функции. Из-за этого метод не мог сослаться на свой класс без кавычек:
class Node:
def __init__(self, value: int, next: Node | None = None) -> None:
self.value = value
self.next = next
def append(self, value: int) -> Node:
return Node(value, self)
print(Node(1).append(2).value)
На Python 3.12 этот код падает с NameError: name 'Node' is not defined, и раньше писали "Node" в кавычках или добавляли в начало файла from __future__ import annotations. С Python 3.14 (PEP 649 и PEP 749) аннотации вычисляются лениво — только когда их кто-то читает, — и код выводит 2.
Ошибка с неизвестным именем никуда не исчезает, она переезжает в момент чтения аннотаций. Типичный пример — импорт только для анализатора под if TYPE_CHECKING::
from typing import TYPE_CHECKING, get_type_hints
from annotationlib import Format, get_annotations
if TYPE_CHECKING:
from decimal import Decimal
def total(price: Decimal, qty: int) -> Decimal:
return price * qty
print(get_annotations(total, format=Format.STRING))
# → {'price': 'Decimal', 'qty': 'int', 'return': 'Decimal'}
get_type_hints(total) # NameError: name 'Decimal' is not defined
Новый модуль annotationlib читает аннотации в трёх форматах: VALUE (вычисленные объекты, как раньше), FORWARDREF (неизвестные имена заменяются на ForwardRef) и STRING (исходный текст). Обычному коду это ничего не меняет. Если вы пишете библиотеку, которая читает аннотации, проверьте её на 3.14.
from __future__ import annotations в 3.14 продолжает работать: с ним аннотации сохраняются строками, как в 3.7–3.13. В новых проектах на 3.14+ он не нужен.
Что нового в typing в Python 3.15
Python 3.15 вышел 9 октября 2026 года. Изменения в typing касаются в основном авторов библиотек:
TypedDictсclosedиextra_items(PEP 728). Закрытый словарь запрещает ключи сверх описанных,extra_itemsразрешает любые дополнительные ключи заданного типа.TypeForm(PEP 747). Аннотация для значений, которые сами являются типами:int,str | None,list[int]. Пригодится функциям видаparse(int, "42")и валидаторам.@typing.disjoint_base(PEP 800). Помечает класс, с которым нельзя множественно наследоваться вместе с другими такими же классами.- Встроенный тип
sentinel(PEP 661). Уникальные значения-маркеры вродеMISSING, которые можно использовать в аннотациях через|.
from typing import TypedDict
class Point(TypedDict, closed=True):
x: int
y: int
class Headers(TypedDict, extra_items=str):
host: str
h: Headers = {"host": "example.com", "accept": "text/html"}
print(Point.__closed__, Headers.__extra_items__) # → True <class 'str'>
Удалены давно устаревшие вещи: декоратор @typing.no_type_check_decorator, создание NamedTuple через именованные аргументы (NamedTuple("Point", x=int)) и пустой TypedDict("TD"). typing.ByteString убран из typing.__all__ и будет удалён в 3.17 — вместо него используйте collections.abc.Buffer или bytes | bytearray. Поддержка новых возможностей в анализаторах появляется с их обновлениями, поэтому перед использованием проверьте свою версию mypy или pyright.
Как проверить типы: mypy, pyright, ty
Аннотации полезны, когда их кто-то проверяет. Основные анализаторы:
| Инструмент | Как установить | Особенности |
|---|---|---|
| mypy | pip install mypy | эталонный анализатор, проверяет только аннотированные функции по умолчанию |
| pyright | расширение Pylance в VS Code или pip install pyright | быстрый, работает прямо в редакторе |
| ty | pip install ty | анализатор от Astral на Rust, в версиях 0.0.x |
Установите mypy в виртуальное окружение проекта и запустите на файле или папке:
py -m pip install mypy
py -m mypy main.py
python3 -m pip install mypy
python3 -m mypy main.py
Чтобы узнать, какой тип анализатор вывел для выражения, используйте reveal_type(). С 3.11 её можно импортировать из typing, тогда код работает и при запуске:
from typing import reveal_type
def first[T](items: list[T]) -> T:
return items[0]
reveal_type(first([1.5, 2.5]))
$ mypy main.py
main.py:6: note: Revealed type is "float"
$ python main.py
Runtime type is 'float'
По умолчанию mypy пропускает тела функций без аннотаций. Флаг --strict включает строгий режим: требует аннотации везде и запрещает вызывать неаннотированные функции из типизированного кода. Для старого проекта начните с обычного режима и включайте строгость по модулям. Для сторонних библиотек без встроенных аннотаций ставят пакеты-заглушки: pip install types-requests, types-PyYAML и т. п.
Аннотации во время выполнения
Python сохраняет аннотации, но не применяет их. Прочитать их можно через get_type_hints() или __annotations__:
def total(price: float, qty: int) -> float:
return price * qty
print(total.__annotations__)
# → {'price': <class 'float'>, 'qty': <class 'int'>, 'return': <class 'float'>}
Проверить значение на обобщённый тип через isinstance() нельзя: isinstance([1, 2], list[int]) выбросит TypeError: isinstance() argument 2 cannot be a parameterized generic. Проверяйте основной тип (isinstance(x, list)), а для настоящей проверки данных по аннотациям используйте Pydantic.
Частые вопросы
Ускоряют или замедляют аннотации программу?
На скорость выполнения в CPython они не влияют: интерпретатор их не использует. В Python 3.14 аннотации ещё и не вычисляются при определении функции, поэтому их стоимость при импорте модуля стала минимальной. Ускорение дают отдельные инструменты, например компилятор mypyc, который превращает аннотированный код в расширение на C.
Нужно ли импортировать List, Dict и Optional из typing?
Нет, если проект работает на Python 3.10 и новее: пишите list[int], dict[str, int] и int | None. List и Dict устарели ещё в 3.9, Optional и Union формально не устарели, но запись через | короче. Все эти имена работают и в 3.15, а линтер Ruff умеет автоматически переписать их на новый синтаксис.
Как аннотировать функцию, которая ничего не возвращает?
-> None — для функции, которая завершается и возвращает None (неявно или через голый return). -> Never (или NoReturn) — для функции, которая никогда не возвращает управление: всегда выбрасывает исключение или вызывает sys.exit().
Аннотации загромождают код. Можно вынести их отдельно?
Да, в stub-файлы с расширением .pyi: рядом с module.py кладут module.pyi с сигнатурами без тел функций, и анализатор берёт типы оттуда. Так типизируют библиотеки на C и чужой код. Для своего кода аннотации в сигнатурах обычно удобнее: их видно при чтении функции, и они не расходятся с реализацией.
Чем TypedDict отличается от dataclass?
TypedDict описывает обычный словарь и существует только для анализатора: удобен для JSON и данных, которые уже приходят словарями. @dataclass создаёт класс с атрибутами, конструктором и сравнением — для объектов, которые вы создаёте сами. Ни один из них не проверяет типы при выполнении.
Какой анализатор выбрать?
Для начала хватит редактора: Pylance в VS Code и PyCharm подсвечивают ошибки типов без настройки. Для проверки в CI (непрерывной интеграции) чаще всего берут mypy — он стабилен и лучше всех задокументирован. pyright и ty работают быстрее на больших проектах; результаты разных анализаторов иногда расходятся в спорных случаях.
Итог
Аннотации типов описывают, какие значения ожидает код: x: int, -> str, list[str], int | None. Python их не проверяет — это делают mypy, pyright и редактор, а библиотеки вроде Pydantic и FastAPI превращают аннотации в проверку данных. Модуль typing нужен для сложных случаев: TypedDict для словарей, Protocol для утиной типизации, Literal, Final, Self, Annotated. В современном Python дженерики пишутся как class Stack[T], псевдонимы — оператором type (3.12), кавычки вокруг ссылок на ещё не объявленные классы не нужны (3.14), а 3.15 добавил закрытые TypedDict и TypeForm. Начните с аннотаций в сигнатурах публичных функций и запускайте mypy — первые найденные ошибки покажут, где типы окупаются.
- #Python
- #typing
- #Аннотации типов
- #mypy
- #Синтаксис