Статическая и динамическая проверка типов средствами typing

Раздел: Основы Python -> Типы данных и аннотации

Основной подход: статическая проверка типов с помощью 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)

Проверка типов с помощью typing в Python - comments

En
Python typing type checking (python)