Type hints: typing, mypy, Optional, Union
Python динамически типизированный - переменные не объявляют тип заранее. Но в больших проектах это создаёт проблемы: невозможно понять что функция принимает и возвращает, IDE не подсказывает методы, ошибки находятся только в runtime. С Python 3.5 в язык добавлены type hints - декларативные аннотации типов, которые не влияют на исполнение, но проверяются статическими анализаторами (mypy, pyright). Это превращает Python в гибрид динамики и статической безопасности.
Базовые аннотации
Аннотация типа пишется после двоеточия:
def greet(name: str) -> str:
return f"Привет, {name}!"
age: int = 30
prices: list = [99.99, 19.99]
Аннотации не влияют на runtime - Python всё равно динамичен:
def greet(name: str) -> str:
return f"Привет, {name}!"
greet(42) # работает! вернёт 'Привет, 42!'
Никакого исключения не будет. Анотации - только для документации и статической проверки.
Базовые типы
n: int = 5
x: float = 3.14
flag: bool = True
name: str = "Alice"
data: bytes = b"binary"
nothing: None = None
Подсказки совпадают с типами Python без специальных импортов.
Контейнеры с типизацией
С Python 3.9 можно использовать встроенные типы как generic:
numbers: list[int] = [1, 2, 3]
prices: dict[str, float] = {"apple": 99.99}
points: tuple[int, int] = (10, 20)
ids: set[int] = {1, 2, 3}
В Python 3.8 и старее нужно из typing:
from typing import List, Dict, Tuple, Set
numbers: List[int] = [1, 2, 3]
prices: Dict[str, float] = {"apple": 99.99}
В 2026 году все проекты на Python 3.11+, поэтому используй встроенные list[int], dict[str, int] - они короче и не требуют импорта.
Optional и Union
Optional[X] - значит «X или None». Почему None стоит отличать от пустой строки и нуля, разбирали в уроке про truthy/falsy:
from typing import Optional
def find_user(id: int) -> Optional[str]:
if id == 1:
return "Alice"
return None
С Python 3.10 появился короткий синтаксис X | None:
def find_user(id: int) -> str | None:
...
Union[X, Y] - «X или Y»:
from typing import Union
def parse_id(value: Union[str, int]) -> int:
return int(value)
# Python 3.10+
def parse_id(value: str | int) -> int:
return int(value)
Синтаксис с | короче и читается естественнее - используй его.
def f(x: Optional[str] = None): # тип допускает None, есть дефолт
def f(x: str | None = None): # то же самое
def f(x: str = None): # тип str не допускает None - mypy ругается
Callable, генераторы и итерации
Для функций:
from typing import Callable
def apply(f: Callable[[int, int], int], a: int, b: int) -> int:
return f(a, b)
apply(lambda x, y: x + y, 1, 2) # 3
Callable[[arg_types], return_type]. Первый параметр - список типов аргументов, второй - тип результата.
Для итераторов:
from typing import Iterator, Generator
def numbers() -> Iterator[int]:
for i in range(10):
yield i
# Generator более подробно (с yield/send/return)
def generator() -> Generator[int, None, None]:
yield 1
Чаще достаточно Iterator или Iterable - как они работают под капотом, в уроке про генераторы.
Любой тип: Any
Any отключает проверку:
from typing import Any
def process(data: Any) -> Any:
return data
Использовать с осторожностью: Any убирает все гарантии. Лучше использовать object если ты явно не знаешь тип, но хочешь хотя бы поверхностную проверку.
Aliases - именованные типы
Длинные типы можно вынести в alias:
from typing import TypeAlias
UserId: TypeAlias = int
UserData: TypeAlias = dict[str, str | int]
def get_user(uid: UserId) -> UserData:
...
С Python 3.12 есть синтаксис без TypeAlias:
type UserId = int
type UserData = dict[str, str | int]
Литеральные типы
Если параметр принимает только конкретные значения:
from typing import Literal
def set_level(level: Literal["debug", "info", "warning", "error"]) -> None:
...
set_level("debug") # OK
set_level("trace") # mypy ошибка
Это полезнее чем строка str - анализатор поймает опечатки.
Final - неперезаписываемые имена
from typing import Final
MAX_RETRIES: Final = 3
MAX_RETRIES = 5 # mypy ошибка
Не превращает в константу в runtime (Python такого не умеет), но даёт сигнал анализатору и читателю.
TypedDict - типизированные словари
Если dict используется как «структура с известными ключами»:
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
def create_user(data: User) -> None:
print(data["name"])
user: User = {"id": 1, "name": "Alice", "email": "a@b.c"}
Альтернатива - dataclass или Pydantic, обычно гибче.
Generic-функции
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
first([1, 2, 3]) # T = int
first(["a", "b"]) # T = str
С Python 3.12 короче:
def first[T](items: list[T]) -> T:
return items[0]
Protocols (structural subtyping)
Утиная типизация с проверкой:
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
def safe_close(resource: Closable) -> None:
resource.close()
# Не требуется наследование - достаточно метода close()
class File:
def close(self) -> None:
print("closed")
safe_close(File()) # OK
В отличие от ABC (явное наследование), Protocol проверяет наличие методов структурно. Полное сравнение - в уроке про ABC и Protocol.
Запуск mypy
pip install mypy
mypy myscript.py
mypy найдёт типичные ошибки:
def greet(name: str) -> str:
return f"Hello, {name}!"
greet(42) # mypy: Argument 1 to "greet" has incompatible type "int"; expected "str"
В реальных проектах mypy запускают в CI - падение типов блокирует мерж. Как собрать связку ruff + mypy, смотри в уроке про линтеры и типы.
Когда писать аннотации
Обязательно:
- Публичные API (функции, методы классов которые вызываются снаружи)
- Параметры функций, которые видны другим разработчикам
- Возвращаемые значения публичных функций
Желательно:
- Сложные локальные переменные (где тип не очевиден)
- Функции с >3 параметрами
Можно опустить:
- Очевидные локальные переменные (
x = 5явно int) - Простые скрипты «на один раз»
- Лямбды и короткие helper-функции
Современная конвенция: типизировать максимум, оставляя локальные очевидности без аннотаций.
Postponed evaluation
Если используешь сам класс в его же аннотациях:
class Node:
def __init__(self, value: int, next: Optional["Node"] = None):
...
Кавычки нужны потому что класс ещё не определён в момент чтения аннотации. С Python 3.10+ можно from __future__ import annotations, и кавычки не нужны:
from __future__ import annotations
class Node:
def __init__(self, value: int, next: Optional[Node] = None):
...
В Python 3.13+ это станет поведением по умолчанию.
Мини-задание
- Аннотируй функцию:
def find_user(users, email):
for user in users:
if user["email"] == email:
return user
return None
# Превратить в:
def find_user(users: list[dict[str, str]], email: str) -> dict[str, str] | None:
...
- Установи mypy и проверь файл:
pip install mypy
echo 'def greet(name: str) -> str: return name + 1' > test_types.py
mypy test_types.py
# error: Unsupported operand types for + ("str" and "int")
- Используй Literal для ограничения значений:
from typing import Literal
LogLevel = Literal["debug", "info", "warning", "error"]
def log(message: str, level: LogLevel = "info") -> None:
print(f"[{level.upper()}] {message}")
log("test") # OK
log("test", "trace") # mypy: Argument 2 to "log" has incompatible type
Что дальше
Модуль 2 завершён - разобрались с типами данных, изменчивостью и аннотациями. В следующем модуле перейдём к управлению потоком: условия, циклы, исключения, match-case и context managers.