Аннотации типов и mypy

⏱ 35 мин · 🧩 заданий: 5

Зачем типы в Python

Python — язык с динамической типизацией: тип есть у значения, а не у переменной. Аннотации типов (PEP 484, Python 3.5+) добавляют подсказки, которые:

  • документируют код: сразу видно, что принимает и возвращает функция;
  • позволяют IDE подсказывать методы и находить ошибки на лету;
  • проверяются статическими анализаторами до запуска (mypy, pyright);
  • используются библиотеками во время выполнения: FastAPI, Pydantic, dataclasses строят валидацию по аннотациям.
def greet(name: str, times: int = 1) -> str:
    return ", ".join([f"Привет, {name}"] * times)

print(greet("Анна", 2))
print(greet.__annotations__)
print(greet(42))                 # интерпретатор НЕ проверяет типы — сработает

Аннотации не проверяются при выполнении

def f(x: int) спокойно примет строку. Ошибки найдёт только mypy/pyright — или библиотека вроде Pydantic, которая читает аннотации явно.

Базовый синтаксис

age: int = 28                          # переменная
price: float = 9.99
name: str
is_active: bool = True

def area(width: float, height: float) -> float:
    return width * height

def log(message: str) -> None:         # ничего не возвращает
    print(message)

Коллекции

С Python 3.9 встроенные типы параметризуются напрямую:

names: list[str] = ["Анна", "Боря"]
scores: dict[str, int] = {"Анна": 90}
unique: set[int] = {1, 2}
point: tuple[float, float] = (1.0, 2.0)      # ровно два элемента
values: tuple[int, ...] = (1, 2, 3)          # любое количество int
matrix: list[list[int]] = [[1, 2], [3, 4]]

Для параметров лучше принимать абстрактные типы из collections.abc — так функция работает с большим числом аргументов:

from collections.abc import Iterable, Sequence, Mapping

def total(items: Iterable[float]) -> float:     # подойдёт list, tuple, генератор
    return sum(items)

def first(items: Sequence[str]) -> str:         # нужна индексация
    return items[0]

def describe(config: Mapping[str, str]) -> str: # только чтение словаря
    return ", ".join(config)

Optional и Union

def find_user(user_id: int) -> str | None:       # Python 3.10+: X | None
    users = {1: "Анна"}
    return users.get(user_id)

def parse(value: str | bytes) -> str:            # одно из нескольких
    return value.decode() if isinstance(value, bytes) else value

user = find_user(2)
if user is not None:                             # сужение типа: дальше user — str
    print(user.upper())
print(parse(b"abc"), parse("xyz"))

Optional[str] из модуля typing — старая запись того же str | None; Union[int, str] — то же, что int | str.

Callable, Any, Literal, Final

from collections.abc import Callable
from typing import Any, Literal, Final

Handler = Callable[[str, int], bool]          # псевдоним типа: функция (str, int) -> bool

def register(event: str, handler: Handler) -> None: ...

def to_json(data: Any) -> str: ...            # Any — проверка отключена

def set_mode(mode: Literal["read", "write"]) -> None: ...   # только эти значения

MAX_RETRIES: Final = 3                        # константа: mypy запретит переприсваивание

mypy

pip install mypy
mypy src/                       # проверить пакет
mypy --strict src/              # строгий режим: требует аннотаций везде
# example.py
def total_price(prices: list[float], discount: float) -> float:
    return sum(prices) * (1 - discount)

total_price([100, 200], "10%")
$ mypy example.py
example.py:4: error: Argument 2 to "total_price" has incompatible type "str"; expected "float"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Полезные приёмы

from typing import cast, reveal_type

x = [1, 2, 3]
reveal_type(x)                  # mypy напечатает: Revealed type is "builtins.list[builtins.int]"

data = cast(dict[str, int], json.loads(text))   # «поверь мне»: подсказка без проверки

result = legacy_function()  # type: ignore[no-untyped-call]   — заглушить конкретную ошибку

Настройка в pyproject.toml

[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = ["legacy.*"]
ignore_errors = true            # старый код — потом

Сужение типов

Анализатор понимает проверки и сужает тип внутри ветки:

def describe(value: int | str | list[int] | None) -> str:
    if value is None:
        return "пусто"
    if isinstance(value, int):          # здесь value: int
        return f"число {value + 1}"
    if isinstance(value, str):          # здесь value: str
        return f"строка {value.upper()}"
    return f"список из {len(value)}"    # здесь value: list[int]

for v in [None, 41, "abc", [1, 2]]:
    print(describe(v))

Постепенная типизация

Типизировать большой проект можно по частям:

  1. Начните с новых модулей и публичных функций.
  2. Запустите mypy в мягком режиме на CI, потом включайте строгие флаги по модулям.
  3. Для библиотек без типов ставьте заглушки: pip install types-requests.
  4. Any — временная мера, а не решение.

pyright и IDE

VS Code (Pylance) использует pyright — он проверяет типы прямо в редакторе. В PyCharm своя проверка. Новые быстрые анализаторы — ty и pyrefly. Многие команды запускают mypy или pyright на CI.

Вопрос на собеседовании

«Проверяет ли Python аннотации типов?» — Нет, интерпретатор их только сохраняет в __annotations__. Проверку делают статические анализаторы (mypy, pyright) до запуска, а библиотеки вроде Pydantic и FastAPI используют аннотации для валидации во время выполнения.

Итог

  • Аннотации: x: int, def f(a: str) -> bool, коллекции list[int], dict[str, float], tuple[int, ...].
  • X | None вместо Optional[X]; для параметров — абстрактные Iterable, Sequence, Mapping.
  • Callable, Literal, Final, Any (отключает проверку) и object.
  • Интерпретатор аннотации не проверяет — это делают mypy/pyright.
  • Сужение типов через is None и isinstance; типизируйте проект постепенно.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Добавьте аннотации типов к функции average(numbers): принимает список чисел с плавающей точкой и возвращает float или None для пустого списка. Используйте современный синтаксис X | None и list[float]. Тест проверит аннотации через __annotations__.

Какие тесты проверяют код (3)
assert average([1.0, 2.0, 3.0]) == 2.0 and average([]) is None
assert average.__annotations__["numbers"] == list[float], "numbers: list[float]"
assert average.__annotations__["return"] == (float | None), "-> float | None"
Задание 2 Программа на Python

Напишите функцию apply_all(funcs, value) с аннотациями: принимает список функций, каждая из которых получает int и возвращает int (Callable[[int], int]), и применяет их к value по очереди. Возвращает int.

Какие тесты проверяют код (3)
assert apply_all([lambda x: x + 1, lambda x: x * 10], 4) == 50
assert apply_all([], 7) == 7
from collections.abc import Callable; assert apply_all.__annotations__["funcs"] == list[Callable[[int], int]]
Задание 3 Один вариант ответа

Что произойдёт при выполнении?

def double(x: int) -> int:
    return x * 2

print(double("ab"))
Задание 4 Заполнить таблицу

Что сообщит mypy?

ошибкавсё в порядке
x: int = "5"
def f(a: float): ... и вызов f(3)
def f() -> str: return None
names: list[str] = [] затем names.append(1)
def f(x: str | None): return x.upper()
def f(x: object): return str(x)
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Чем Any отличается от object в аннотациях?

Войдите, чтобы отмечать пройденные уроки.