Skip to main content
pythonIntermediate50 минут

Практический гайд по статической типизации в Python

Справочник по статической типизации: аннотации, функции, TypedDict, Protocols и настройка mypy

#python#typing#mypy#static typing#TypedDict#Protocol#type hints

Практический гайд по статической типизации в Python

Типы не ускоряют код, но ускоряют команду: меньше багов, меньше контекстных переключений, предсказуемые ревью.

Зачем это бизнесу и команде

  • Тимлид: ускоряет код-ревью за счет явных контрактов и автодополнения
  • Разработчик: уверенный рефакторинг и навигация по коду, как в TypeScript
  • Бизнес: меньше runtime-ошибок и инцидентов в продакшене
  • Команда: самодокументируемые API и прозрачные контракты между сервисами

Как пользоваться материалом

  • Спешишь? Прочти введение, блок про функции и чек-лист mypy.
  • Любишь примерчики? Их много, копируй смело.
  • Внедряешь аккуратно? Добавляй аннотации постепенно — начни с публичных API, затем покрывай внутренние функции.

Путь обучения

  • Новичок: Базовые типы → Функции → mypy → первые аннотации в проекте
  • Продвинутый: Protocols → Generics → Overload → строгие правила проверки
  • Архитектор: TypedDict → проектирование контрактов → CI/CD и соглашения по типам

Введение

Статическая типизация в Python — это возможность указывать типы данных для переменных, параметров функций и возвращаемых значений, что помогает:

  • Обнаруживать ошибки на этапе разработки
  • Улучшать читаемость кода
  • Упрощать рефакторинг
  • Получать лучшую IDE-поддержку

Люблю TypeScript за предсказуемость контрактов, поэтому переношу те же подходы в Python: типы делают навигацию и ревью такими же уверенными.

Как работает типизация в Python

Python использует gradual typing — вы можете добавлять типы постепенно, и они не влияют на выполнение кода в runtime. Это гибрид: хочешь строго — будь строгим, хочешь свободы — оставь конкретные участки динамичными, но делай это осознанно.

Типичные ошибки, которые типизация ловит

# Без типизации: баг спрячется в runtime
def calculate_discount(items: list[dict], discount_rate: float) -> float:
    total = sum(item["price"] for item in items)  # quantity легко забыть
    return total * discount_rate
 
# С типизацией: контракт делает проблему явной
from typing import TypedDict
 
class CartItem(TypedDict):
    name: str
    price: float
    quantity: int
 
def calculate_discount_safe(items: list[CartItem], discount_rate: float) -> float:
    total = sum(item["price"] * item["quantity"] for item in items)
    return total * discount_rate
  • Не тот ключ или None там, где ожидали число
  • Перепутанные аргументы при вызове функций
  • Несовпадение типов в контракте API (строка вместо int)
  • Ошибки после рефакторинга, которые сразу подсвечивает mypy

До/после: эволюция кода

# Без типов: баги проявляются в проде
def ship(order):
    return order["items"] * order["price"]
 
# Базовая типизация: IDE и ревью защищают от очевидных ошибок
def ship(order: dict[str, float]) -> float:
    return order["items"] * order["price"]
 
# Продвинутая типизация: четкий контракт и защита от пропусков полей
from typing import TypedDict, Literal
 
class Order(TypedDict):
    id: int
    items: int
    price: float
    status: Literal["new", "paid", "shipped"]
 
def ship(order: Order) -> Order:
    if order["status"] != "paid":
        raise ValueError("Order not paid")
    return {**order, "status": "shipped"}

Базовые типы и аннотации

Теперь, когда понятна ценность и базовые принципы, разберем основу — аннотации встроенных типов.

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

Мини-правило №1: сначала добавь типы на входах/выходах функций. Переменные внутри можно добавить позже — это упрощает ревью кода и улучшает поддержку в IDE.

# Аннотации переменных
name: str = "Иван"
age: int = 25
height: float = 1.75
is_student: bool = True
 
# Коллекции
from typing import List, Dict, Set, Tuple, Optional
 
names: List[str] = ["Анна", "Борис", "Мария"]
scores: Dict[str, int] = {"математика": 95, "физика": 87}
unique_ids: Set[int] = {1, 2, 3, 4, 5}
coordinates: Tuple[float, float] = (55.7558, 37.6173)
 
# Может быть None
optional_name: Optional[str] = None

Современный синтаксис (Python 3.9+)

# Вместо typing.List, Dict и т.д.
names: list[str] = ["Анна", "Борис"]
scores: dict[str, int] = {"математика": 95}
unique_ids: set[int] = {1, 2, 3}
coordinates: tuple[float, float] = (55.7558, 37.6173)

Пойманная ошибка новичка: Optional[str] — это str | None, а не «необязательный аргумент». Делай def f(name: Optional[str] = None) -> None.

Типизация функций

Базовые типы покрыты, теперь перейдем к поведению: функции и их сигнатуры — главный контракт модуля.

Базовые примеры

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

from typing import Any, Optional
 
def greet(name: str) -> str:
    return f"Привет, {name}!"
 
def calculate_sum(numbers: list[int]) -> int:
    return sum(numbers)
 
def process_user(
    user_id: int,
    name: str,
    email: Optional[str] = None,
) -> dict[str, Any]:
    user_data = {"id": user_id, "name": name}
    if email:
        user_data["email"] = email
    return user_data

Более сложные случаи

Ниже — приёмы, где типизатор дает дополнительные гарантии: Union/| заставляет поддерживать оба варианта, Callable проверяет сигнатуру передаваемой функции, TypeVar сохраняет связь вход/выход без Any, а Optional требует явной проверки None.

from typing import Union, Callable, Any, TypeVar, Generic
 
# Union типы (или новый синтаксис | в Python 3.10+)
def process_value(value: Union[int, str]) -> str:
    return str(value)
 
# Python 3.10+
def process_value_modern(value: int | str) -> str:
    return str(value)
 
# Функции как аргументы
def apply_operation(
    numbers: list[int],
    operation: Callable[[int], int],
) -> list[int]:
    return [operation(x) for x in numbers]
 
# Generics
T = TypeVar("T")
U = TypeVar("U")
 
class Result(Generic[T, U]):
    def __init__(self, value: T, metadata: U):
        self.value = value
        self.metadata = metadata
 
def process_result(result: Result[int, str]) -> str:
    return f"Value: {result.value}, Meta: {result.metadata}"

Где нужны TypeVar? Там, где хочется универсальности без Any: общие контейнеры, репозитории, кэшеры.

Overload: разные сигнатуры одной функции

from typing import overload
 
@overload
def process_data(data: str) -> list[str]: ...
 
@overload
def process_data(data: int) -> dict[str, int]: ...
 
def process_data(data: str | int) -> list[str] | dict[str, int]:
    if isinstance(data, str):
        return data.split()
    return {"value": data}

Literal и Final для контрактов

Literal ограничивает значение заданным набором строк/чисел — типизатор подсветит опечатку в аргументе. Final фиксирует константу: ее нельзя переопределить, и это защищает от случайных изменений в коде.

from typing import Literal, Final
 
HttpMethod = Literal["GET", "POST", "PUT", "DELETE"]
API_URL: Final[str] = "https://api.example.com"
 
def make_request(method: HttpMethod, url: str = API_URL) -> None:
    # mypy проверит, что method валиден, а константа API_URL не изменяется
    ...

Структурные типы

Классы с типизацией

Dataclass + типы = документация, которая обновляется сама. Добавь ClassVar для констант, чтобы mypy не ругался на «статические поля».

from dataclasses import dataclass
from typing import ClassVar, Optional
 
@dataclass
class Person:
    name: str
    age: int
    email: Optional[str] = None
    species: ClassVar[str] = "Homo sapiens"
 
    def get_info(self) -> str:
        info = f"{self.name}, {self.age} лет"
        if self.email:
            info += f" ({self.email})"
        return info
 
class Student(Person):
    student_id: str
    grades: list[float]
 
    def __init__(self, name: str, age: int, student_id: str):
        super().__init__(name, age)
        self.student_id = student_id
        self.grades = []
 
    def add_grade(self, grade: float) -> None:
        self.grades.append(grade)
 
    def get_average(self) -> float:
        return sum(self.grades) / len(self.grades) if self.grades else 0.0

TypedDict

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

Когда нужно словарь «как объект, но без класса». Идеально для JSON-данных и контрактов между слоями.

Базовое использование

from typing import TypedDict, NotRequired
 
class User(TypedDict):
    id: int
    username: str
    email: str
    age: NotRequired[int]  # Python 3.11+
    is_active: bool
 
# Альтернативный синтаксис для Python < 3.11
from typing import TypedDict as LegacyTypedDict
 
class UserLegacy(LegacyTypedDict):
    id: int
    username: str
    email: str
    is_active: bool
 
# Total=False для всех необязательных полей
class PartialUser(TypedDict, total=False):
    id: int
    username: str
    email: str

Не путай total=False с Optional: поле может отсутствовать, но если присутствует — обязано быть нужного типа.

Практические примеры

from typing import List, Any
 
class Address(TypedDict):
    street: str
    city: str
    postal_code: str
    country: str
 
class UserProfile(TypedDict):
    user_id: int
    username: str
    addresses: List[Address]
    preferences: dict[str, Any]
 
def create_user_profile() -> UserProfile:
    return {
        "user_id": 123,
        "username": "ivan_petrov",
        "addresses": [
            {
                "street": "Ленина 15",
                "city": "Москва",
                "postal_code": "101000",
                "country": "Россия",
            }
        ],
        "preferences": {
            "theme": "dark",
            "language": "ru",
        },
    }
 
def validate_user_data(data: dict) -> bool:
    """Проверяет, соответствуют ли данные типу User"""
    required_fields = {"id", "username", "email", "is_active"}
    return all(field in data for field in required_fields)
 
# Использование с функциями
def process_user_data(user_data: User) -> None:
    print(f"Обработка пользователя: {user_data['username']}")
    if "age" in user_data:
        print(f"Возраст: {user_data['age']}")

Наследование TypedDict

class BaseUser(TypedDict):
    id: int
    username: str
 
class AdminUser(BaseUser):
    permissions: list[str]
    is_superuser: bool
 
class CustomerUser(BaseUser):
    orders_count: int
    loyalty_points: float
 
def get_user_type(user_data: BaseUser) -> str:
    if "permissions" in user_data:
        return "admin"
    if "orders_count" in user_data:
        return "customer"
    return "base"

Работа с JSON и валидацией

import json
from typing import Any, TypedDict, cast
 
class UserData(TypedDict):
    id: int
    name: str
    email: str
 
def parse_user_response(json_data: str) -> UserData:
    data = json.loads(json_data)
    # Безопасное приведение типа: mypy проверит ключи при доступе
    return cast(UserData, data)
 
# Альтернатива с проверкой полей
def safe_parse_user_response(json_data: str) -> UserData:
    data = json.loads(json_data)
    if not all(key in data for key in ("id", "name", "email")):
        raise ValueError("Missing required fields")
    return UserData(
        id=int(data["id"]),
        name=str(data["name"]),
        email=str(data["email"]),
    )
  • TypedDict фиксирует ожидаемые ключи
  • cast подходит для узких мест, но лучше добавлять явную проверку полей
  • В больших проектах используйте pydantic или dataclasses + валидацию, но аннотации сохраняйте для mypy

Generics на практике

Когда базовые аннотации освоены, переходите к обобщенным контейнерам — это снимает соблазн использовать Any.

from dataclasses import dataclass
from typing import TypeVar, Generic, Sequence
 
@dataclass
class User:
    name: str
 
@dataclass
class Product:
    name: str
    price: float
 
T = TypeVar("T")
 
class Paginator(Generic[T]):
    """Универсальный пагинатор для любых моделей"""
 
    def __init__(self, items: Sequence[T], page_size: int):
        self.items = items
        self.page_size = page_size
 
    def get_page(self, page: int) -> Sequence[T]:
        start = (page - 1) * self.page_size
        return self.items[start : start + self.page_size]
 
# mypy сохранит типы при использовании
users = [User("Alice"), User("Bob")]
first_page = Paginator(users, 10).get_page(1)  # Sequence[User]
 
products = [Product("Book", 15.99), Product("Pen", 2.99)]
product_page = Paginator(products, 5).get_page(1)  # Sequence[Product]

Protocols

TypedDict фиксирует структуру данных, а Protocols — поведение. Они реализуют структурную типизацию (утиную типизацию) в статической типизации.

Protocols — «если ходит как утка и крякает как утка, mypy согласится». Не нужно наследоваться, главное — совпасть по сигнатурам.

Базовые Protocol

from typing import Protocol, runtime_checkable
 
class SupportsRead(Protocol):
    def read(self, size: int = -1) -> bytes:
        ...
 
    def close(self) -> None:
        ...
 
class SupportsWrite(Protocol):
    def write(self, data: bytes) -> int:
        ...
 
    def close(self) -> None:
        ...
 
class ReadableAndWritable(SupportsRead, SupportsWrite):
    pass
 
def process_file(file_like: SupportsRead) -> bytes:
    try:
        return file_like.read()
    finally:
        file_like.close()
 
# Примеры классов, которые удовлетворяют протоколу
class SimpleReader:
    def read(self, size: int = -1) -> bytes:
        return b"some data"
 
    def close(self) -> None:
        print("Closed")
 
class NetworkStream:
    def read(self, size: int = -1) -> bytes:
        return b"network data"
 
    def write(self, data: bytes) -> int:
        return len(data)
 
    def close(self) -> None:
        print("Network connection closed")

Более сложные примеры

from typing import Iterator, Iterable, Optional
 
class DatabaseConnection(Protocol):
    def execute(self, query: str, params: tuple = ()) -> Iterator[tuple]:
        ...
 
    def commit(self) -> None:
        ...
 
    def rollback(self) -> None:
        ...
 
    def close(self) -> None:
        ...
 
class Repository(Protocol):
    connection: DatabaseConnection
 
    def find_by_id(self, id: int) -> Optional[dict]:
        ...
 
    def save(self, entity: dict) -> int:
        ...
 
    def delete(self, id: int) -> bool:
        ...
 
# Реализация
class PostgreSQLConnection:
    def __init__(self, dsn: str):
        self.dsn = dsn
 
    def execute(self, query: str, params: tuple = ()) -> Iterator[tuple]:
        # Реализация для PostgreSQL
        yield from []
 
    def commit(self) -> None:
        print("Commit")
 
    def rollback(self) -> None:
        print("Rollback")
 
    def close(self) -> None:
        print("Close PostgreSQL connection")
 
class UserRepository:
    def __init__(self, connection: DatabaseConnection):
        self.connection = connection
 
    def find_by_id(self, id: int) -> Optional[dict]:
        # Реализация поиска
        return None
 
    def save(self, entity: dict) -> int:
        # Реализация сохранения
        return 1
 
    def delete(self, id: int) -> bool:
        # Реализация удаления
        return True
 
# Функция, работающая с любым репозиторием
def backup_repository(repo: Repository) -> list[dict]:
    """Создает backup данных из репозитория"""
    results = []
    # Логика backup
    return results

Runtime проверка с Protocol

from typing import Any
 
@runtime_checkable
class Serializable(Protocol):
    def to_dict(self) -> dict[str, Any]:
        ...
 
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Serializable":
        ...
 
class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
 
    def to_dict(self) -> dict[str, Any]:
        return {"name": self.name, "age": self.age}
 
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "User":
        return cls(data["name"], data["age"])
 
def serialize_data(obj: Serializable) -> dict[str, Any]:
    return obj.to_dict()
 
# Runtime проверка
user = User("Иван", 30)
if isinstance(user, Serializable):
    data = serialize_data(user)
    print(data)

Инструменты и практика

Настройка mypy

Создайте mypy.ini или pyproject.toml:

# mypy.ini
[mypy]
python_version = 3.11
warn_return_any = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
 
# Игнорировать отсутствующие импорты в этих модулях
[mypy-requests.*]
ignore_missing_imports = True

mypy — строгая проверка: она подсветит подозрительные места еще до запуска. Начни с малого: warn_return_any=True и disallow_untyped_defs=True.

Интеграция с IDE

VS Code:

  • Установите расширение Pylance
  • Настройки для settings.json:
{
  "python.analysis.typeCheckingMode": "basic|strict"
}

PyCharm:

  • Включите inspection "Type checker"
  • Используйте встроенную поддержку типов

Чек-лист внедрения

  • Установить mypy: pip install mypy
  • Добавить аннотации хотя бы к 10 ключевым функциям/методам
  • Настроить mypy.ini с минимальными проверками (см. выше)
  • Подключить pre-commit hook с mypy
  • Запускать mypy в CI/CD и блокировать мердж при ошибках

Интеграция в CI/CD пайплайн

# .github/workflows/ci.yml
jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
      - run: pip install mypy
      - run: mypy .
        # Постепенно повышайте строгость
        # mypy --strict .

Готовность к продакшену

  • mypy --strict проходит без ошибок
  • Все публичные API имеют аннотации
  • # type: ignore только с поясняющими комментариями
  • CI блокирует мердж при ошибках типов
  • Новые PR содержат типы для новой логики

Практические советы

  1. Начинайте постепенно:
# Сначала без типов
def old_function(data):
    return process(data)
 
# Затем добавляйте типы
def new_function(data: dict) -> list:
    return process(data)
 
# И наконец — конкретные типы
def typed_function(data: UserProfile) -> list[User]:
    return process_users(data)
  1. Используйте TypeAlias для сложных типов:
from typing import Dict, List, TypeAlias, Union
 
# Python 3.10+
JsonValue: TypeAlias = Union[
    str, int, float, bool, None, List["JsonValue"], Dict[str, "JsonValue"]
]
JsonObject: TypeAlias = Dict[str, JsonValue]

Типизация исключений

from typing import NoReturn
import logging
import sys
 
def validate_age(age: int) -> None:
    if age < 0:
        raise ValueError("Age cannot be negative")
    # Возвращает None для валидных значений — значит, NoReturn не подходит
 
# NoReturn только для функций, которые всегда завершаются исключением/exit
def fatal_error(message: str) -> NoReturn:
    """Функция, которая гарантированно не возвращает управление.
 
    Используется для обработчиков критических ошибок, где продолжение
    выполнения программы невозможно.
    """
    logging.critical(message)
    sys.exit(1)
  • NoReturn применяйте только к функциям, которые гарантированно не возвращаются
  • Для валидаторов, которые иногда бросают исключение, используйте -> None
  1. Обработка сторонних библиотек:
# Создайте stub-файлы или используйте ignore
import some_untyped_library  # type: ignore
 
# Или создайте .pyi файлы

Шаги без стресса: аннотируй публичные функции → включи mypy в CI → при каждом рефакторинге смотри на подсказки. Старт получается структурированным, без резких движений.

Когда статическая типизация избыточна

  • Быстрые прототипы и исследовательский код, где API меняется каждый час
  • Короткие скрипты на десятки строк, где overhead превышает пользу
  • Сильно динамический код (метапрограммирование, плагины), где сигнатуры плавают
  • Очень старый легаси: иногда проще обернуть слой адаптерами, чем типизировать всё

Антипаттерны и частые ошибки

# ПЛОХО: избыточные аннотации без смысла
name: str = "Иван"
 
# ПЛОХО: игнор без пояснений
value = fetch()  # type: ignore  # ← добавляй комментарий, почему это безопасно
 
# ПЛОХО: слишком сложные типы без алиасов
def process(data: dict[str, list[tuple[int, str]]]) -> None: ...
 
# ХОРОШО: вводим алиас и документируем намерение
ComplexData = dict[str, list[tuple[int, str]]]
def process(data: ComplexData) -> None: ...
  • Не ставьте type: ignore без комментария — иначе это технический долг.
  • Не аннотируйте каждую переменную, если тип очевиден из присваивания.
  • Не держите огромные вложенные типы без алиасов — это ухудшает читаемость.

Сторонние библиотеки и stub-файлы

  • Сначала ищите готовые типы: pip install types-requests или types-PyYAML
  • Если библиотеки без типов — создайте typings/packagename/__init__.pyi
  • Для временного решения используйте # type: ignore с комментарием «нет stubs»
# typings/some_untyped_library/__init__.pyi
def send(payload: dict[str, str]) -> bool: ...

Добавьте путь к typings в mypy_path, и mypy начнет использовать ваши заглушки.

Бенчмарки: стоимость vs выгода

  • Runtime-накладных расходов нет: аннотации стираются в байткоде
  • Прогон mypy в CI на среднем сервисе (100–200 модулей) обычно укладывается в десятки секунд
  • После покрытия типов ревью проходят заметно быстрее благодаря автодополнению и явным контрактам
  • Часть дефектов ловится до продакшена за счет статической проверки и IDE-подсветки
  • from __future__ import annotations ускоряет импорт и снижает риск циклических ссылок на типы

Миграция легаси-кода

  1. Включите check_untyped_defs=True и аннотируйте публичные функции модулей
  2. Добавьте TypedDict/Protocol на пограничных слоях: входящие данные, HTTP, БД
  3. Расставьте type: ignore с комментариями там, где нет stubs, и заведите задачи на закрытие
  4. Введите правило: новая логика без типов не принимается в ревью
  5. Подключите mypy в pre-commit → затем в CI, постепенно повышая строгость

Инструменты и альтернативы

  • mypy — де-факто стандарт; --strict для максимальной строгости
  • pyright — быстрый чекер от Microsoft, строгий по умолчанию
  • pytype — инструмент от Google, хорошо подходит для Python 3.11+
  • pylance — движок для VS Code, дает подсветку и навигацию
  • pydantic — runtime-валидация и сериализация; хорошо для API/DTO
  • attrs/dataclasses — удобные data-классы; комбинируйте с аннотациями для статической проверки
  • TypedDict — минимальные накладные расходы для структур JSON

Быстрый выбор инструмента

  • Нужна максимальная совместимость и сообщество → выбирайте mypy
  • Нужна скорость и строгий режим из коробки → pyright
  • Нужен анализ «как выполняется код» и inference в стиле Google → pytype

Интеграция с FastAPI и Django

# FastAPI: типы превращаются в валидацию схем
from fastapi import FastAPI
from pydantic import BaseModel
 
class Item(BaseModel):
    id: int
    name: str
    price: float
 
app = FastAPI()
 
@app.post("/items")
def create_item(item: Item) -> Item:
    return item
 
# Django: типы в сервисах и ORM-слое
from typing import Iterable
from myapp.models import Order
 
def get_paid_orders(ids: Iterable[int]) -> list[Order]:
    return list(Order.objects.filter(id__in=ids, status="paid"))
 
# Flask: TypedDict для входных данных, dataclass для ответов
from dataclasses import asdict, dataclass
from typing import TypedDict
from flask import Flask, jsonify, request
 
class CreateUserPayload(TypedDict):
    name: str
    email: str
 
@dataclass
class UserDTO:
    id: int
    name: str
    email: str
 
app_flask = Flask(__name__)
 
@app_flask.post("/users")
def create_user_endpoint():
    payload: CreateUserPayload = request.get_json(force=True)
    user = UserDTO(id=1, name=payload["name"], email=payload["email"])
    return jsonify(asdict(user)), 201

Типы делают контракты FastAPI самодокументируемыми, в Django помогают ловить несоответствия при работе с ORM и сервисными функциями, а во Flask фиксируют структуру входящих payload’ов.

Ошибки, которые статическая проверка не поймает

  • Ошибки конфигурации и окружения (не тот URL, секреты)
  • Динамическое отражение и getattr по строкам
  • Нарушение инвариантов в runtime (пустой список, но мы не проверили)
  • Логика, завязанная на данные из внешних сервисов, если нет явной валидации

Решение частых проблем

Circular import при типизации

from typing import TYPE_CHECKING
 
if TYPE_CHECKING:
    from .models import User
 
def get_user() -> "User": ...

Типы для декораторов

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  # ✅ без ignore
  • Добавляйте узкие type: ignore[...] с пояснениями, если нужно
  • Для сложных декораторов используйте ParamSpec и Concatenate

Пример полного приложения

from typing import Protocol, TypedDict, List, Optional
from dataclasses import dataclass
 
class ProductData(TypedDict):
    id: int
    name: str
    price: float
    category: str
    in_stock: bool
 
class ProductRepository(Protocol):
    def find_by_id(self, product_id: int) -> Optional[ProductData]:
        ...
 
    def find_by_category(self, category: str) -> List[ProductData]:
        ...
 
    def save(self, product: ProductData) -> int:
        ...
 
class NotificationService(Protocol):
    def send_low_stock_alert(self, product: ProductData) -> bool:
        ...
 
class DatabaseProductRepository:
    def find_by_id(self, product_id: int) -> Optional[ProductData]:
        # Реализация работы с БД
        return None
 
    def find_by_category(self, category: str) -> List[ProductData]:
        return []
 
    def save(self, product: ProductData) -> int:
        return 1
 
class EmailNotificationService:
    def send_low_stock_alert(self, product: ProductData) -> bool:
        print(f"Отправка уведомления для {product['name']}")
        return True
 
class ProductService:
    def __init__(
        self,
        repository: ProductRepository,
        notification_service: NotificationService,
    ):
        self.repository = repository
        self.notification_service = notification_service
 
    def update_product_stock(
        self,
        product_id: int,
        new_stock: bool,
    ) -> Optional[ProductData]:
        product = self.repository.find_by_id(product_id)
        if product:
            updated_product = {**product, "in_stock": new_stock}
            self.repository.save(updated_product)
 
            if not new_stock:
                self.notification_service.send_low_stock_alert(updated_product)
 
            return updated_product
        return None
 
    def get_products_by_category(self, category: str) -> List[ProductData]:
        return self.repository.find_by_category(category)
 
def main():
    repository = DatabaseProductRepository()
    notifications = EmailNotificationService()
    service = ProductService(repository, notifications)
 
    products = service.get_products_by_category("electronics")
    for product in products:
        print(f"Product: {product['name']} - ${product['price']}")
 
if __name__ == "__main__":
    main()

Заключение

Статическая типизация в Python — это мощный инструмент, который:

  • Улучшает качество кода
  • Помогает находить ошибки на ранних этапах
  • Упрощает поддержку больших проектов
  • Обеспечивает лучшую документацию

Начните с простых аннотаций и постепенно переходите к более сложным конструкциям вроде Protocols и TypedDict. Используйте mypy для проверки типов и интегрируйте типизацию в ваш процесс разработки.

Практический гайд по статической типизации в Python — Learning Center — Potapov.me