Типизация переменных: полное руководство по 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 - список строк.