Аннотации типов в Python
Аннотации типов Python: как аннотировать переменные, функции и классы, использовать модуль typing и проверять типы с помощью mypy.
Аннотации типов позволяют указывать ожидаемый тип для переменных, параметров функций и возвращаемых значений. Python не проверяет их во время выполнения — они являются метаданными, которые используются редакторами, линтерами и инструментами статического анализа, такими как mypy, чтобы находить ошибки до запуска кода.
В этой главе рассматривается:
- Зачем нужны аннотации типов и когда их использовать
- Аннотирование переменных и функций
- Встроенные типы и модуль
typing(List,Dict,Optional,Union,Tuple,Any,Callable) - Современный синтаксис (Python 3.10+)
- Аннотирование классов и
self - Обобщения и псевдонимы типов
- Статический анализ с помощью mypy
- Распространённые ловушки
Зачем нужны аннотации типов?
Python — динамически типизированный язык: переменная может хранить значение любого типа. Эта гибкость очень удобна, но в крупных кодовых базах становится сложно разобраться — по месту вызова функции нельзя узнать тип её аргумента.
Аннотации типов решают эту проблему, не лишая Python его динамизма:
- Редакторы сразу показывают ошибки. VS Code, PyCharm и другие подчёркивают несоответствия типов прямо во время набора кода.
- Рефакторинг становится безопаснее. Измените сигнатуру функции, и инструмент проверки типов укажет на все места вызова, которые перестанут работать.
- Код становится самодокументирующимся.
def greet(name: str) -> strсразу показывает контракт без docstring. - Библиотеки становится проще использовать. Типизированные библиотеки предоставляют автодополнение для каждого атрибута и метода.
Аннотации типов были введены в Python 3.5 через PEP 484. Синтаксис совершенствовался в каждом крупном выпуске с тех пор. В примерах ниже указывается минимальная версия Python, начиная с которой доступен соответствующий синтаксис.
Аннотирование переменных
Добавьте двоеточие после имени переменной, а затем укажите тип:
name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = TrueМожно также объявить тип переменной без немедленного присваивания значения. Это называется опережающим объявлением и полезно внутри классов или на уровне модуля:
user_id: int # declared but not yet assigned
user_id = 42Аннотации типов на уровне модуля не влияют на поведение во время выполнения — они сохраняются в словаре __annotations__ модуля, но интерпретатором игнорируются.
Аннотирование функций
Добавляйте аннотации к параметрам (после двоеточия) и к возвращаемому значению (после -> перед двоеточием, завершающим сигнатуру):
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
def send_email(to: str, subject: str, body: str) -> None:
print(f"Sending '{subject}' to {to}")
result: int = add(3, 5)
message: str = greet("Alice")-> None означает, что функция не возвращает значимого результата (она неявно возвращает None). Опустить аннотацию возвращаемого значения тоже допустимо, но явное -> None делает намерение понятным.
Параметры по умолчанию
Значения по умолчанию указываются после аннотации:
def connect(host: str, port: int = 8080, secure: bool = False) -> None:
print(f"Connecting to {host}:{port} (secure={secure})")
connect("example.com") # uses defaults
connect("example.com", 443, True)*args и **kwargs
Аннотируйте тип элемента, а не тип коллекции:
def total(*prices: float) -> float:
return sum(prices)
def create_user(**fields: str) -> dict:
return fields
print(round(total(9.99, 4.50, 12.00), 2)) # 26.49
print(create_user(name="Bob", role="admin"))*prices: float означает, что каждый позиционный аргумент является float; во время выполнения prices по-прежнему остаётся обычным tuple из float. Аналогично, **fields: str означает, что значение каждого именованного аргумента является str.
Модуль typing
Для всего, что выходит за рамки базовых встроенных типов, выполняйте импорт из модуля typing (Python 3.5+). Начиная с Python 3.9, многие типы из typing были объединены непосредственно со встроенными аналогами (см. Современный синтаксис ниже).
List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict
def first_names(users: List[str]) -> str:
return users[0] if users else ""
def dimensions() -> Tuple[int, int, int]:
return (1920, 1080, 32)
def unique_tags(items: List[str]) -> Set[str]:
return set(items)
def word_count(text: str) -> Dict[str, int]:
counts: Dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
print(first_names(["Alice", "Bob"])) # Alice
print(dimensions()) # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"])) # {'py', 'web'}
print(word_count("one two one")) # {'one': 2, 'two': 1}Optional
Optional[X] — сокращение для Union[X, None]. Используйте его, когда значение может отсутствовать:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id) # returns None if not found
name = find_user(1)
if name is not None:
print(name.upper()) # ALICE
missing = find_user(99)
print(missing) # NoneИнструмент проверки типов видит Optional[str] и знает, что перед вызовом строковых методов на результате нужно проверить None. Без такой проверки он сообщит об ошибке.
Union
Union[X, Y] означает, что значение может быть либо типа X, либо типа Y:
from typing import Union
def stringify(value: Union[int, float, str]) -> str:
return str(value)
print(stringify(42)) # 42
print(stringify(3.14)) # 3.14
print(stringify("hi")) # hiUnion наиболее полезен, когда функция действительно принимает несколько несвязанных типов. Если вы пишете Union[str, None], используйте вместо этого Optional[str] — это более идиоматично.
Callable
Callable[[ArgTypes...], ReturnType] аннотирует функцию, передаваемую в качестве аргумента:
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(n: int) -> int:
return n * 2
print(apply_twice(double, 3)) # 12Callable[[int], int] означает: вызываемый объект, принимающий один аргумент типа int и возвращающий int. Если список аргументов сложен или неизвестен, используйте Callable[..., ReturnType].
Any
Any — специальный тип, который отключает проверку типов для данного значения. Любой тип как присваивается в Any, так и присваивается из Any:
from typing import Any
def log(value: Any) -> None:
print(value)
log(42)
log("hello")
log([1, 2, 3])Используйте Any осторожно — это аварийный выход, который устраняет саму защиту, которую обеспечивают аннотации типов. Он уместен при работе с нетипизированным сторонним кодом или в ходе постепенной миграции большой кодовой базы.
Современный синтаксис (Python 3.9+, 3.10+)
Встроенные обобщения (Python 3.9+)
Начиная с Python 3.9, встроенные типы можно использовать напрямую как обобщения, без импорта из typing:
# Python 3.9+
def word_count(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def first(items: list[int]) -> int | None:
return items[0] if items else None
print(word_count("cat dog cat")) # {'cat': 2, 'dog': 1}
print(first([10, 20, 30])) # 10
print(first([])) # NoneИспользуйте list[str] вместо List[str], dict[str, int] вместо Dict[str, int] и т. д.
Синтаксис объединения X | Y (Python 3.10+)
В Python 3.10 введён оператор | для объединений, заменяющий Union[X, Y] и Optional[X]:
# Python 3.10+
def parse(value: str | int | None) -> str:
if value is None:
return "nothing"
return str(value)
print(parse("hello")) # hello
print(parse(42)) # 42
print(parse(None)) # nothingstr | None эквивалентно Optional[str]. Этот синтаксис чище и легче читается.
Аннотирование классов
Аннотируйте атрибуты экземпляра внутри __init__ и добавляйте аннотации возвращаемых значений к методам:
class BankAccount:
owner: str # class-level annotation (no default value)
balance: float
def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0)) # True
print(account) # BankAccount(owner='Alice', balance=120.00)Аннотация self всегда выводится автоматически — вы никогда не пишете self: BankAccount. Возвращаемый тип __init__ всегда равен None.
ClassVar
Используйте ClassVar[T] (из typing) для обозначения атрибута, принадлежащего классу, а не каждому экземпляру:
from typing import ClassVar
class Config:
MAX_RETRIES: ClassVar[int] = 3
timeout: int
def __init__(self, timeout: int) -> None:
self.timeout = timeout
print(Config.MAX_RETRIES) # 3Инструмент проверки типов предупредит, если вы попытаетесь установить ClassVar для экземпляра — он предназначен для совместного использования на уровне класса.
Псевдонимы типов
Псевдоним типа даёт длинному или сложному типу более короткое и понятное имя:
from typing import List, Tuple
# Simple alias
UserID = int
Filename = str
# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]
def distance(p1: Coordinates, p2: Coordinates) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
print(distance((0.0, 0.0), (3.0, 4.0))) # 5.0Начиная с Python 3.12, для явных, инспектируемых псевдонимов используйте оператор type:
# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]Обобщения с TypeVar
TypeVar позволяет написать одну функцию, работающую с любым типом, при этом сохраняя связи между типами:
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
x: int = first_item([1, 2, 3]) # x is int
s: str = first_item(["a", "b"]) # s is strИнструмент проверки типов определяет из аргумента, что такое T, и переносит эту информацию на возвращаемый тип. Без TypeVar пришлось бы возвращать Any и терять безопасность типов.
Можно ограничить TypeVar набором допустимых типов:
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float)
def double(n: Numeric) -> Numeric:
return n * 2
print(double(4)) # 8 (int)
print(double(2.5)) # 5.0 (float)Статическая проверка типов с помощью mypy
mypy — наиболее широко используемый инструмент статической проверки типов для Python. Установите его с помощью pip:
pip install mypyЗатем запустите его на файле:
mypy my_script.pyПример: обнаружение ошибки с помощью mypy
Сохраните следующий код как demo.py:
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet(42) # passing int instead of str
print(result.upper())Запуск mypy demo.py выводит:
demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)Python сам по себе выполнит этот код без ошибок (f-строки преобразуют любой тип), но mypy обнаружил несоответствие до того, как вы столкнулись бы с ним в продакшне.
Полезные параметры mypy
| Флаг | Эффект |
|---|---|
--strict | Включить все дополнительные проверки (рекомендуется для новых проектов) |
--ignore-missing-imports | Подавить ошибки об отсутствующих stub-файлах сторонних библиотек |
--check-untyped-defs | Также проверять функции без аннотаций |
--disallow-untyped-defs | Требовать аннотации во всех определениях функций |
Файл mypy.ini (или секция [tool.mypy] в pyproject.toml) позволяет не указывать конфигурацию в командной строке:
[mypy]
strict = true
ignore_missing_imports = trueПостепенная типизация
Аннотировать все функции сразу не обязательно. Python поддерживает постепенную типизацию: аннотированный и неаннотированный код мирно сосуществуют. По умолчанию mypy пропускает неаннотированные функции (если не задан --check-untyped-defs).
Практический подход для существующей кодовой базы:
- Добавляйте аннотации ко всему новому коду с первого дня.
- Аннотируйте в первую очередь наиболее часто вызываемые или наиболее проблемные функции.
- Включайте
--strictпо одному модулю по мере увеличения покрытия. - Используйте
Anyтолько там, где сторонняя библиотека не типизирована, и добавляйте комментарий с объяснением.
Распространённые ловушки
Опережающие ссылки
Если тип ссылается на класс, определённый позже в том же файле, оберните имя в кавычки, чтобы сделать его строкой (опережающей ссылкой):
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
head = Node(1, Node(2))
print(head.value, head.next.value) # 1 2Начиная с Python 3.10+, добавьте from __future__ import annotations в начало файла. Это делает все аннотации ленивыми строками и устраняет необходимость в ручном оборачивании в кавычки.
Аннотации во время выполнения
По умолчанию в Python 3.9 и более ранних версиях аннотации вычисляются немедленно. Это означает, что опережающая ссылка без кавычек вызовет NameError:
# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...При использовании from __future__ import annotations (Python 3.7+) все аннотации сохраняются как строки и вычисляются только при обращении к ним — это автоматически решает проблему опережающих ссылок.
None vs Optional
Распространённая ошибка — аннотировать возвращаемый тип как str, тогда как функция на самом деле может вернуть None. Всегда используйте Optional[str] (или str | None), когда None является возможным возвращаемым значением:
from typing import Optional
# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
if user_id == 0:
return None # type: ignore — this is the bug
# Correct
def get_name_safe(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "Alice"list vs List (совместимость версий)
Если ваш код работает на Python 3.8 или более ранней версии, необходимо использовать from typing import List и писать List[str]. В Python 3.9+ list[str] работает напрямую. Если нужно поддерживать обе версии, либо используйте импорты из typing, либо добавьте from __future__ import annotations.
Краткий справочник
| Аннотация | Значение |
|---|---|
x: int | Переменная x является целым числом |
def f(a: str) -> bool | Параметр a — str; возвращаемое значение — bool |
-> None | Функция не возвращает значимого результата |
Optional[str] | str или None |
Union[int, str] | int или str |
list[int] / List[int] | Список целых чисел |
dict[str, int] / Dict[str, int] | Словарь с ключами str и значениями int |
tuple[int, str] / Tuple[int, str] | Кортеж из (int, str) |
Callable[[int], str] | Функция, принимающая int и возвращающая str |
Any | Любой тип (проверка отключена) |
ClassVar[T] | Атрибут уровня класса |
TypeVar("T") | Обобщённая переменная типа |
Связанные темы
- Функции Python — место, где живут аннотации параметров и возвращаемых типов.
- Классы и объекты Python — аннотирование
__init__, методов и атрибутов класса. - Датаклассы Python — аннотации типов обязательны для объявления полей датаклассов.
- Абстрактные классы Python — абстрактные базовые классы естественно сочетаются с аннотациями типов.