Аннотации типов Python: полное практическое пособие

Раздел: Стиль кода -> Type hints

Основные принципы аннотации типов

Современный Python (начиная с версии 3.10) позволяет использовать оператор | для объединения типов, что заменяет устаревшую конструкцию Union. Это делает код более лаконичным и интуитивно понятным.

def greet(name: str | None) -> str:
    if name is None:
        return "Hello, stranger!"
    return f"Hello, {name}!"

Python типизация переменных (типизация переменных в python)

Также активно используется list, dict и другие встроенные дженерики без импорта из typing (доступно с Python 3.9).

Типичные ошибки:

  • Забыть импортировать Optional или Union в проектах, поддерживающих Python < 3.10.
  • Использовать List[int] вместо list[int] в коде, рассчитанном на Python 3.9+, что усложняет чтение.
  • Путать Optional[int] и int | None (семантически одинаково, но вариант с | короче).

Как аннотировать простые переменные и функции?

Для переменных и функций используются базовые типы: int, float, str, bool, bytes и т.д.

x: int = 42
y: str = "hello"

def add(a: int, b: int) -> int:
    return a + b

аннотация типов python (аннотация типов в python)

Проблема: аннотации не проверяются во время выполнения. Ошибки типов обнаруживаются только статическими анализаторами (mypy, pyright).

Как указать, что функция ничего не возвращает или возвращает разные типы?

Используйте None или Union/| для нескольких типов.

def log(message: str) -> None:
    print(message)

def get_status(code: int) -> str | int:
    if code == 200:
        return "OK"
    return code

Ошибка:

Неверное указание возвращаемого типа может привести к ложным срабатываниям анализатора.

Как аннотировать коллекции (списки, словари, кортежи)?

В Python 3.9+ коллекции аннотируются напрямую: list[int], dict[str, float]. Для версий ниже используйте typing.List, typing.Dict.

from typing import List, Dict

# современный способ (Python 3.9+)
scores: list[int] = [90, 85, 88]
prices: dict[str, float] = {"apple": 1.5, "banana": 0.8}

# устаревший (Python < 3.9)
scores_old: List[int] = [90, 85, 88]

Проблема: для кортежей фиксированной длины используйте tuple[int, str, float], для переменной длины - tuple[int, ...].

Как обрабатывать необязательные параметры и None?

Используйте Optional (до Python 3.10) или | None.

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    # возвращает имя или None
    return "Alice" if user_id == 1 else None

# эквивалент с |
def find_user_new(user_id: int) -> str | None:
    return "Alice" if user_id == 1 else None

Ошибка:

Не путать Optional[int] (эквивалент Union[int, None]) с необязательным аргументом def func(x: int = 5) - это разные концепции.

Как аннотировать функции с *args и **kwargs?

Для *args используется кортеж элементов, для **kwargs - словарь.

def sum_all(*args: int) -> int:
    return sum(args)

def print_kwargs(**kwargs: str) -> None:
    for key, value in kwargs.items():
        print(f"{key}: {value}")

Проблема: для kwargs часто указывают общий тип значения (str), но это не даёт контроля над отдельными ключами.

Как использовать дженерики (TypeVar) для обобщённого кода?

TypeVar позволяет создавать функции, работающие с любым типом, сохраняя типизацию.

from typing import TypeVar

T = TypeVar('T')

def first_element(items: list[T]) -> T:
    return items[0]

# пример
result = first_element([1, 2, 3])  # result: int
result2 = first_element(["a", "b"])  # result: str

Ошибка: забыть привязать TypeVar к конкретному классу (T = TypeVar('T', bound=int)) может привести к нежелательным операциям.

Как аннотировать callable (функции высшего порядка)?

Используйте Callable[[arg_types], return_type] из typing.

from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

result = apply(lambda x, y: x + y, 3, 4)  # 7

Проблема: для сложных сигнатур с *args используйте Callable[..., ReturnType].

Как аннотировать self и возвращаемый тип в классах?

Для self обычно не указывают тип, но при необходимости можно использовать Self (Python 3.11+).

from typing import Self

class MyClass:
    def set_value(self, value: int) -> Self:
        self.value = value
        return self

Ошибка: в версиях до 3.11 для возврата экземпляра класса используют "MyClass" (строковая аннотация) или TypeVar.

Как использовать Protocol для структурной типизации (утиная типизация)?

Protocol позволяет определить минимальный набор методов/атрибутов, которым должен обладать объект.

from typing import Protocol

class Flyable(Protocol):
    def fly(self) -> None: ...

def make_fly(obj: Flyable) -> None:
    obj.fly()

class Bird:
    def fly(self) -> None:
        print("Bird flying")

make_fly(Bird())  # корректно

Проблема: Protocol не проверяется во время выполнения; статический анализатор может пропустить ошибку, если метод не реализован.

Расширенные примеры аннотаций типов

1. Аннотации с использованием Literal (Python 3.8+)

Тип Literal позволяет ограничить значение конкретными литералами (строками, числами, булевыми).

Пример
from typing import Literal

def set_mode(mode: Literal["read", "write", "append"]) -> str:
    if mode == "read":
        return "Opening file for reading"
    elif mode == "write":
        return "Opening file for writing"
    else:
        return "Opening file for appending"

print(set_mode("read"))
Opening file for reading

Если передать недопустимое значение, статический анализатор выдаст ошибку.

2. TypedDict для строгой типизации словарей

TypedDict определяет структуру словаря с конкретными ключами и типами значений.

Пример
from typing import TypedDict

class Person(TypedDict):
    name: str
    age: int
    email: str | None

def process_person(p: Person) -> str:
    return f"{p['name']} (age {p['age']})"

data: Person = {"name": "Alice", "age": 30, "email": None}
print(process_person(data))
Alice (age 30)

Примечание:

TypedDict не проверяется во время выполнения, только статически. Для проверки во время выполнения можно использовать dataclass.

3. ParamSpec и Concatenate для декораторов, сохраняющих сигнатуру (Python 3.10+)

ParamSpec позволяет декоратору передавать все параметры оборачиваемой функции, сохраняя их типы.

Пример
from typing import Callable, ParamSpec, TypeVar

P = ParamSpec('P')
T = TypeVar('T')

def log_call(func: Callable[P, T]) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a: int, b: int) -> int:
    return a + b

result = add(2, 3)
print(result)
Calling add
5

4. Аннотации с использованием Self (Python 3.11+) для возврата текущего класса

Self упрощает паттерн builder и цепочки методов.

Пример
from typing import Self

class Builder:
    def __init__(self):
        self._items: list[str] = []

    def add(self, item: str) -> Self:
        self._items.append(item)
        return self

    def build(self) -> str:
        return ', '.join(self._items)

b = Builder().add("apple").add("banana")
print(b.build())
apple, banana

5. Аннотации для асинхронных функций (async/await)

Асинхронные функции аннотируются как обычные, но возвращаемый тип обычно Coroutine или конкретный тип с обёрткой.

Пример
import asyncio
from typing import Coroutine

async def fetch_data(url: str) -> dict:
    # имитация запроса
    await asyncio.sleep(1)
    return {"status": 200, "data": "ok"}

async def main() -> None:
    result = await fetch_data("http://example.com")
    print(result)

asyncio.run(main())
{'status': 200, 'data': 'ok'}

Если функция возвращает Coroutine, можно указать типы передаваемых и возвращаемых значений: Coroutine[Any, Any, dict].

6. Аннотации с использованием TypeGuard (Python 3.10+)

TypeGuard позволяет пользовательской функции проверки типа сужать тип переменной для анализатора.

Пример
from typing import TypeGuard

def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process(items: list[object]) -> None:
    if is_string_list(items):
        # теперь items считается list[str]
        print(' '.join(items))
    else:
        print("Not all strings")

process(["hello", "world"])
hello world

7. Аннотации для исключений (raise/except) с помощью Never (Python 3.11+)

Тип Never указывает, что функция никогда не завершается нормально (например, всегда выбрасывает исключение).

Пример
from typing import Never

def abort(message: str) -> Never:
    raise SystemExit(message)

def risky() -> int:
    abort("Fatal error")
    return 0  # этот код недостижим

8. Аннотации с использованием overload (перегрузка функций)

Декоратор @overload позволяет описать несколько сигнатур одной функции для разных типов аргументов.

Пример
from typing import overload

@overload
def process(data: int) -> str: ...

@overload
def process(data: str) -> int: ...

def process(data: int | str) -> str | int:
    if isinstance(data, int):
        return str(data)
    else:
        return len(data)

print(process(42))   # str
print(process("hi")) # int
42
2

Перегрузки улучшают автодополнение и проверку типов.

Аннотация типов в Python - comments

En
аннотация типов python (python)