Аннотации типов и mypy
Зачем типы в 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))
Постепенная типизация
Типизировать большой проект можно по частям:
- Начните с новых модулей и публичных функций.
- Запустите mypy в мягком режиме на CI, потом включайте строгие флаги по модулям.
- Для библиотек без типов ставьте заглушки:
pip install types-requests. 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
Добавьте аннотации типов к функции 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"
def average(numbers: list[float]) -> float | None:
if not numbers:
return None
return sum(numbers) / len(numbers)
Напишите функцию 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]]
from collections.abc import Callable
def apply_all(funcs: list[Callable[[int], int]], value: int) -> int:
for func in funcs:
value = func(value)
return value
Что произойдёт при выполнении?
def double(x: int) -> int:
return x * 2
print(double("ab"))
Что сообщит 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) |
Чем Any отличается от object в аннотациях?
Войдите, чтобы отмечать пройденные уроки.