Практический гайд по статической типизации в Python
Справочник по статической типизации: аннотации, функции, TypedDict, Protocols и настройка mypy
Оглавление
Практический гайд по статической типизации в 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.0TypedDict
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 resultsRuntime проверка с 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 = Truemypy — строгая проверка: она подсветит подозрительные места еще до запуска.
Начни с малого: 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-commithook с 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 содержат типы для новой логики
Практические советы
- Начинайте постепенно:
# Сначала без типов
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)- Используйте 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
- Обработка сторонних библиотек:
# Создайте 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ускоряет импорт и снижает риск циклических ссылок на типы
Миграция легаси-кода
- Включите
check_untyped_defs=Trueи аннотируйте публичные функции модулей - Добавьте TypedDict/Protocol на пограничных слоях: входящие данные, HTTP, БД
- Расставьте
type: ignoreс комментариями там, где нет stubs, и заведите задачи на закрытие - Введите правило: новая логика без типов не принимается в ревью
- Подключите 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 для проверки типов и интегрируйте типизацию в ваш процесс разработки.