Типизация переменных: полное руководство по Python Type Hints

Раздел: Продвинутый Python -> Type Hints

Типизация переменных в Python с помощью аннотаций (type hints) позволяет указать ожидаемые типы данных, улучшая читаемость кода и позволяя статическим анализаторам (mypy, pyright) находить ошибки до выполнения. Аннотации не влияют на работу программы в рантайме, но служат документацией и основой для проверки.

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

Самый эффективный и современный способ указать тип переменной или функции использовать синтаксис аннотаций, введённый в Python 3. Для переменных после имени ставится двоеточие и тип:

name: str = "Alice"
age: int = 30
is_active: bool = True

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

В функциях аннотируются параметры и возвращаемое значение:

def greet(name: str) -> str:
    return f"Hello, {name}"

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

Для классов можно аннотировать атрибуты экземпляра и методы:

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name: str = name
        self.age: int = age

При проверке mypy команда mypy script.py выявит несоответствия, например, попытку передать число в параметр, ожидающий строку.

Типичные проблемы:

  • Аннотации игнорируются, если не установлен и не запущен статический анализатор. Ошибки типов не проявляются в рантайме.
  • При использовании типов из модуля typing (например, List, Dict) в Python 3.8 и ниже требуется импорт from typing import List; в Python 3.9+ можно использовать встроенные list, dict.
  • Циклические импорты решаются строковыми аннотациями: def method(self) -> "MyClass": ... или импортом from __future__ import annotations.

Варианты решения типовых задач

Как указать, что переменная может быть None или иметь другой тип?

Используйте Union из модуля typing или оператор | (Python 3.10+). Для краткого обозначения Optional[X] равнозначно Union[X, None].

from typing import Union, Optional

# Python 3.10+
result: int | None = None
# Старый синтаксис
result2: Union[int, None] = None
result3: Optional[int] = None

Проблемы:

  • В Python 3.9 и ниже оператор | для объединения типов не поддерживается (вызовет TypeError).
  • Путаница: Optional[int] означает Union[int, None], а не необязательный аргумент функции (для необязательных аргументов используется значение по умолчанию None с аннотацией Optional).

Как аннотировать список чисел или словарь строк?

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

from typing import List, Dict, Tuple

# Python 3.9+
names: list[str] = ["Anna", "Bob"]
stats: dict[str, float] = {"avg": 3.5}
pair: tuple[str, int] = ("key", 42)

# Старый синтаксис
names_old: List[str] = ["Anna"]
stats_old: Dict[str, float] = {"avg": 3.5}

Проблемы:

  • Использование list[int] в Python 3.8 вызовет TypeError: 'type' object is not subscriptable.
  • Вложенные Generic: list[list[int]] или Dict[str, List[int]] обязательны для точности.

Как создать функцию, работающую с любым типом, но возвращающую тот же тип?

Примените TypeVar – переменную типа, которая связывает тип входа и выхода. Определите одну или несколько таких переменных.

from typing import TypeVar

T = TypeVar('T')

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

# Пример использования
first([1, 2, 3])  # mypy выводит int
first(["a", "b"])  # mypy выводит str

Проблемы:

  • TypeVar не ограничивает типы в рантайме. В теле функции нельзя полагаться на методы конкретного класса без проверки.
  • При использовании нескольких TypeVar необходимо следить за правильной привязкой, иначе mypy может вывести Any.

Как описать поведение объекта без явного наследования?

Используйте Protocol (структурная типизация). Определите класс, наследующий от Protocol, и перечислите требуемые методы/атрибуты. Любой объект, имеющий их, будет совместим.

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None: ...

def cleanup(obj: SupportsClose) -> None:
    obj.close()

# mypy не выдаст ошибку, если передан объект с методом close
cleanup(open("file.txt"))  # file object has close
cleanup(42)  # ошибка: int не имеет close

Проблемы:

  • По умолчанию Protocol не проверяется в рантайме. Для этого нужно добавить декоратор @runtime_checkable и вызывать isinstance.
  • При изменении сигнатуры метода (например, другой набор параметров) совместимость теряется.

Как задать строгие типы для словаря с известными ключами?

Применяйте TypedDict. Определите класс, где каждый ключ аннотирован. По умолчанию все ключи обязательны; для необязательных используйте total=False.

from typing import TypedDict

class Movie(TypedDict):
    title: str
    year: int
    rating: float | None  # Python 3.10+

# Обязательные ключи: title, year; rating может отсутствовать
film: Movie = {"title": "Inception", "year": 2010, "rating": 8.8}

Проблемы:

  • TypedDict не проверяет лишние ключи; может быть добавлен ключ, не объявленный.
  • При доступе к необязательному ключу mypy может предупредить, что значение может быть None.

Как ограничить переменную только определёнными литералами?

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

from typing import Literal

def open_file(mode: Literal["r", "w", "a"]) -> str:
    # реализация
    return mode

open_file("r")  # OK
open_file("x")  # ошибка mypy: Argument 1 has incompatible type "x"

Проблемы:

  • Literal требует константные значения; переменные не принимаются.
  • Сложно использовать с enum или вычисляемыми константами.

Как пометить константу, чтобы mypy не разрешал её переопределение?

Примените аннотацию Final. Переменная с ней не может быть переприсвоена (с точки зрения статической проверки).

from typing import Final

MAX_SIZE: Final = 100
# mypy: Final name cannot be reassigned
MAX_SIZE = 200  # ошибка

Проблемы:

  • Final проверяется только статически; в рантайме переопределение возможно.
  • В классах атрибут с Final не может быть переопределён в подклассе.

Как описать разное возвращаемое значение в зависимости от аргументов?

Используйте декоратор @overload. Несколько копий сигнатуры функции, а затем одна реализация без аннотаций типа (или с Any).

from typing import overload

@overload
def parse(data: str) -> dict: ...
@overload
def parse(data: bytes) -> list: ...
def parse(data):
    if isinstance(data, str):
        return {"source": data}
    else:
        return [b for b in data]

Mypy подберёт нужную сигнатуру в зависимости от типа аргумента.

Проблемы:

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

Как добавить типы в код, не используя синтаксис Python 3?

В унаследованных проектах на Python 2 или для обратной совместимости применялись комментарии-аннотации вида # type: тип.

x = 10  # type: int
def greet(name):  # type: (str) -> str
    return "Hello " + name

Современные анализаторы (mypy) всё ещё поддерживают этот стиль с флагом --no-site-packages.

Проблемы:

  • Загромождает код, сложнее читается.
  • Не поддерживает продвинутые конструкции (Generic, Protocol) без дополнительного синтаксиса.

Практические примеры продвинутой типизации

TypeVar с ограничением (bound)

Если требуется, чтобы тип поддерживал определённые методы (например, сравнение), укажите bound:

Пример
from typing import TypeVar

T = TypeVar('T', bound=int | float)

def add(x: T, y: T) -> T:
    return x + y

print(add(5, 10))   # 15
print(add(3.0, 4.5)) # 7.5
# print(add("a", "b"))  # mypy: incompatible type
Результат mypy: ошибка для строк, для чисел нет.

Protocol с обобщённым типом

Protocol может быть параметризован TypeVar, чтобы описать контейнер с методом, возвращающим элемент:

Пример
from typing import Protocol, TypeVar

T = TypeVar('T')

class Gettable(Protocol[T]):
    def get(self, index: int) -> T: ...

def first_item(container: Gettable[T]) -> T:
    return container.get(0)

class MyList:
    def __init__(self, items):
        self._items = items
    def get(self, index: int):
        return self._items[index]

result = first_item(MyList([1, 2, 3]))  # T выводится как int
Тип result определён как int.

TypedDict с необязательными ключами и наследованием

Можно создать базовый TypedDict и расширять его, а также частично делать ключи необязательными:

Пример
from typing import TypedDict, NotRequired  # Python 3.11+

class BaseMovie(TypedDict):
    title: str
    year: int

class ExtendedMovie(BaseMovie):
    rating: NotRequired[float]

movie: ExtendedMovie = {"title": "Matrix", "year": 1999}  # rating не обязателен
movie2: ExtendedMovie = {"title": "Matrix", "year": 1999, "rating": 8.7}
Mypy не выдаёт ошибок.

Перегрузка функций с Union и модификаторами

Используйте перегрузку, чтобы разный ввод давал разный результат, но при этом реализация общая:

Пример
from typing import overload, Union

@overload
def serialize(data: int) -> str: ...
@overload
def serialize(data: list) -> bytes: ...
def serialize(data: Union[int, list]) -> Union[str, bytes]:
    if isinstance(data, int):
        return str(data)
    return bytes(str(data), 'utf-8')

x = serialize(42)  # mypy: str
y = serialize([1,2])  # mypy: bytes
x: str, y: bytes.

Опасность типа Any

Тип Any отключает проверку типов. Использовать его следует только для динамического кода или при миграции:

Пример
from typing import Any

def process(data: Any) -> None:
    # mypy не будет проверять типы внутри
    print(data.nonexistent())  # ошибка только в рантайме

process(42)  # будет AttributeError
Mypy не выдаст предупреждения, программа упадёт.

Annotated для метаданных (Python 3.11+)

Позволяет прикрепить к типу дополнительную информацию (например, для валидации):

Пример
from typing import Annotated

# Первый аргумент – тип, второй и далее – метаданные
PositiveFloat = Annotated[float, "must be > 0"]

def set_temperature(value: PositiveFloat) -> None:
    pass

# mypy игнорирует метаданные, но их можно использовать в рантайме
Метаданные доступны через __metadata__.

Self для методов, возвращающих экземпляр класса (Python 3.11+)

Позволяет корректно типизировать цепочки методов:

Пример
from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self
    def build(self) -> str:
        return str(self.name)

obj = Builder().set_name("test").build()  # mypy знает, что возвращается Builder
Цепочка работает без ошибок типов.

TypeGuard для пользовательских проверок (Python 3.10+)

Позволяет сужать тип после проверки:

Пример
from typing import TypeGuard, Any

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

def process(items: list[Any]):
    if is_str_list(items):
        # здесь items сужается до list[str]
        print(" ".join(items))  # OK
После проверки mypy считает, что items - список строк.

типизация переменных в Python - comments

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