W3docs

Аннотации типов в 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"))    # hi

Union наиболее полезен, когда функция действительно принимает несколько несвязанных типов. Если вы пишете 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))   # 12

Callable[[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))      # nothing

str | 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).

Практический подход для существующей кодовой базы:

  1. Добавляйте аннотации ко всему новому коду с первого дня.
  2. Аннотируйте в первую очередь наиболее часто вызываемые или наиболее проблемные функции.
  3. Включайте --strict по одному модулю по мере увеличения покрытия.
  4. Используйте 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Параметр astr; возвращаемое значение — 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")Обобщённая переменная типа

Связанные темы

Практика

Практика
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Практика
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Практика
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?