Статическая и динамическая проверка типов средствами typing
Основной подход: статическая проверка типов с помощью mypy
Статический анализатор mypy
Для проверки корректности аннотаций типов без выполнения программы используется инструмент mypy. Он анализирует исходный код и выявляет несоответствия между объявленными типами и фактическим использованием.
# Установка: pip install mypy
# Пример файла example.py
from typing import List
def sum_list(numbers: List[int]) -> int:
return sum(numbers)
result = sum_list([1, 2, 3])
print(result) # 6
Python type self (тип self в python)
Запуск mypy:
$ mypy example.py Success: no issues found in 1 source file
Python typing function (аннотация типа функции в python)
Если добавить ошибку:
def sum_list(numbers: List[int]) -> int:
return sum(numbers) + "строка"
Python 3.12 type (новые возможности типов в python 3.12)
$ mypy example.py example.py:3: error: Unsupported operand type(s) for +: "int" and "str" Found 1 error in 1 file (checked 1 source file)
Python typing type checking (проверка типов с помощью typing в python)
Mypy находит несоответствие типов до запуска программы, что особенно полезно в больших проектах.
Типичные проблемы:
- Mypy игнорирует файлы без аннотаций – необходимо включать проверку явно через
--strict. - Аннотации могут быть неверными, если код использует динамические структуры (например,
**kwargs). - Совместимость с библиотеками без аннотаций – требуется stub-файлы или
# type: ignore.
Как проверить типы во время выполнения без внешних инструментов?
Для динамической проверки типов в рантайме применяются встроенные функции isinstance() и assert. Это не заменяет статический анализ, но полезно при отладке или для валидации входных данных.
def add(a: int, b: int) -> int:
assert isinstance(a, int) and isinstance(b, int), "Аргументы должны быть int"
return a + b
print(add(2, 3)) # 5
print(add("a", 3)) # AssertionError
Python type dict (тип dict в python)
Можно использовать typing.get_type_hints() для получения аннотаций и последующей проверки:
from typing import get_type_hints
def func(x: int, y: str) -> bool:
pass
hints = get_type_hints(func)
print(hints) # {'x': int, 'y': str, 'return': bool}
Python return type (тип возврата функции в python)
Проблемы: isinstance не проверяет сложные типы (например, List[int]) – работает только для простых. Для проверки параметризованных типов нужны дополнительные библиотеки (например, pydantic).
Как автоматически валидировать типы в классах данных с помощью pydantic?
Библиотека pydantic предоставляет декораторы и базовый класс BaseModel, автоматически проверяющий типы при создании объекта.
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str = ""
user = User(name="Анна", age=30)
print(user) # name='Анна' age=30 email=''
# Ошибка при неверном типе
try:
User(name="Петр", age="двадцать")
except Exception as e:
print(e) # 1 validation error for User...
Pydantic поддерживает сложные типы (List, Optional, Union), кастомные валидаторы, а также генерирует JSON Schema.
Может быть избыточным для простых скриптов. Добавляет зависимость и увеличивает время импорта. Не подходит для критичных ко времени выполнения участков.
Как использовать Protocol для структурной типизации (утиная типизация с проверкой)?
Протоколы (PEP 544) позволяют определить интерфейс без явного наследования. Проверка происходит по структуре (наличию методов/атрибутов).
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "Рисуем круг"
class Square:
def draw(self) -> str:
return "Рисуем квадрат"
def render(obj: Drawable) -> None:
print(obj.draw())
render(Circle()) # Рисуем круг
render(Square()) # Рисуем квадрат
Mypy проверит, что переданный объект реализует метод draw с правильной сигнатурой.
Не работает с динамически добавляемыми методами. Protocol не проверяется в рантайме (только статически, mypy). Для рантайм-проверки нужна библиотека типа runtime_protocol или abc.
Как создавать обобщенные функции и классы с помощью TypeVar?
TypeVar позволяет параметризовать типы, гарантируя согласованность типов внутри функции/класса без привязки к конкретному типу.
from typing import TypeVar, List
T = TypeVar('T')
def first_element(lst: List[T]) -> T:
return lst[0]
print(first_element([1, 2, 3])) # 1 (int)
print(first_element(['a', 'b'])) # 'a' (str)
Также можно ограничивать TypeVar с помощью bound (например, Number = TypeVar('Number', int, float)).
TypeVar не влияет на рантайм – это только подсказка для статических анализаторов. Ошибки с несовместимыми типами (например, first_element([1, "2"])) mypy обнаружит, но выполнение не прервется.
Как использовать TypedDict для строго типизированных словарей?
TypedDict (PEP 589) позволяет задать структуру словаря с заданными ключами и их типами.
from typing import TypedDict
class Book(TypedDict):
title: str
year: int
def process_book(book: Book) -> str:
return f"{book['title']} ({book['year']})"
book = {'title': 'Война и мир', 'year': 1869}
print(process_book(book)) # Война и мир (1869)
Mypy предупредит, если ключ отсутствует или имеет неверный тип.
TypedDict – это синтаксический сахар для статической проверки; в рантайме это обычный dict. Нельзя проверять наличие ключей динамически.
Расширенные примеры использования typing
Generic с пользовательскими ограничениями
Создадим класс контейнера, который принимает только числовые типы.
from typing import Generic, TypeVar, Union
Numeric = TypeVar('Numeric', int, float)
class Vector(Generic[Numeric]):
def __init__(self, x: Numeric, y: Numeric) -> None:
self.x = x
self.y = y
def __add__(self, other: 'Vector[Numeric]') -> 'Vector[Numeric]':
return Vector(self.x + other.x, self.y + other.y)
v1 = Vector(1.0, 2.0)
v2 = Vector(3.0, 4.0)
v3 = v1 + v2
print(v3.x, v3.y) # 4.0 6.0
# v4 = Vector("a", "b") # mypy укажет на ошибку: str не входит в Numeric
4.0 6.0
Декоратор с сохранением аннотаций типов
Используем ParamSpec и TypeVar для создания типобезопасного декоратора.
from typing import Callable, ParamSpec, TypeVar, Any
P = ParamSpec('P')
R = TypeVar('R')
def log_call(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a: int, b: int) -> int:
return a + b
add(2, 3) # Вызов add с args=(2, 3), kwargs={}
Вызов add с args=(2, 3), kwargs={}
Использование Literal для ограничения возможных значений
Literal позволяет указать конкретные литералы, которые может принимать аргумент.
from typing import Literal
def set_mode(mode: Literal['read', 'write', 'append']) -> str:
return f"Режим установлен: {mode}"
print(set_mode('read')) # Режим установлен: read
# set_mode('delete') # mypy: Argument 1 to "set_mode" has incompatible type "Literal['delete']"
Режим установлен: read
Создание псевдонимов типов с NewType
NewType создает новый тип, который отличается от базового для статической проверки, но в рантайме это тот же тип.
from typing import NewType
UserId = NewType('UserId', int)
PostId = NewType('PostId', int)
def get_user(user_id: UserId) -> str:
return f"Пользователь {user_id}"
uid = UserId(42)
print(get_user(uid)) # Пользователь 42
# pid = PostId(10)
# get_user(pid) # mypy: Argument 1 to "get_user" has incompatible type "PostId"; expected "UserId"
Пользователь 42
Ковариантность и контравариантность в Generics
Определение ковариантного (с covariant=True) и контравариантного (с contravariant=True) типа. Полезно при работе с коллекциями или функциями.
from typing import Generic, TypeVar, List
T_co = TypeVar('T_co', covariant=True) # ковариантный
T_contra = TypeVar('T_contra', contravariant=True) # контравариантный
class Container(Generic[T_co]):
def __init__(self, value: T_co) -> None:
self._value = value
def get_value(self) -> T_co:
return self._value
# Container[int] является подтипом Container[float], так как int -> float ковариантен
class Handler(Generic[T_contra]):
def handle(self, event: T_contra) -> None:
pass
# Handler[float] является подтипом Handler[int], так как float -> int контравариантен (функция от float может принимать int)
Рекурсивные типы (например, для дерева)
Использование ForwardRef или строковых аннотаций для ссылки на себя.
from typing import List, Optional, ForwardRef
TreeNode = ForwardRef('TreeNode') # или from __future__ import annotations
class TreeNode:
def __init__(self, value: int, children: Optional[List['TreeNode']] = None) -> None:
self.value = value
self.children = children or []
# Mypy может потребовать from __future__ import annotations для отложенной оценки
Combining typing with dataclasses and frozen
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Point:
x: float
y: float
@dataclass
class Line:
start: Point
end: Point
points: List[Point] = None
line = Line(Point(0,0), Point(1,1))
print(line)
Line(start=Point(x=0.0, y=0.0), end=Point(x=1.0, y=1.0), points=None)