Python поддерживает два подхода к типизации: номинальную (наследование) и структурную (duck typing). Protocol формализует duck typing, позволяя проверять совместимость типов по наличию методов и атрибутов, а не по иерархии наследования.
Protocol -- структурная типизация
Проблема без Protocol
# Without Protocol — must use inheritance
from abc import ABC, abstractmethod
class Printable(ABC):
@abstractmethod
def to_string(self) -> str: ...
class User(Printable): # Must explicitly inherit!
def __init__(self, name: str) -> None:
self.name = name
def to_string(self) -> str:
return f"User({self.name})"
# What about classes from third-party libraries?
# Can't make them inherit from Printable!
Решение: Protocol
from typing import Protocol
class Printable(Protocol):
"""Any object with a to_string() method."""
def to_string(self) -> str: ...
class User:
"""User class — does NOT inherit from Printable."""
def __init__(self, name: str) -> None:
self.name = name
def to_string(self) -> str:
return f"User({self.name})"
class Product:
"""Product class — also does NOT inherit from Printable."""
def __init__(self, title: str) -> None:
self.title = title
def to_string(self) -> str:
return f"Product({self.title})"
# Both work — structural compatibility, no inheritance needed
def display(item: Printable) -> None:
print(item.to_string())
display(User("Alice")) # OK — User has to_string()
display(Product("Laptop")) # OK — Product has to_string()
Protocol с атрибутами
from typing import Protocol
class HasName(Protocol):
"""Any object with a 'name' attribute."""
name: str
class HasSize(Protocol):
"""Any object with a 'size' property."""
@property
def size(self) -> int: ...
class Document:
def __init__(self, name: str, content: str) -> None:
self.name = name
self.content = content
@property
def size(self) -> int:
return len(self.content)
def greet(entity: HasName) -> str:
return f"Hello, {entity.name}!"
def is_large(entity: HasSize) -> bool:
return entity.size > 1000
doc = Document("readme", "Hello world")
print(greet(doc)) # OK — Document has .name
print(is_large(doc)) # OK — Document has .size property
Комбинирование Protocol
from typing import Protocol, runtime_checkable
class Readable(Protocol):
def read(self) -> str: ...
class Writable(Protocol):
def write(self, data: str) -> None: ...
class ReadWritable(Readable, Writable, Protocol):
"""Combines both protocols."""
pass
# runtime_checkable allows isinstance checks
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
class FileWrapper:
def read(self) -> str:
return "data"
def write(self, data: str) -> None:
pass
def close(self) -> None:
pass
# isinstance works with runtime_checkable Protocol
wrapper = FileWrapper()
print(isinstance(wrapper, Closeable)) # True
TypedDict -- типизированные словари
TypedDict описывает структуру словаря с конкретными ключами и типами:
from typing import TypedDict, NotRequired
class UserDict(TypedDict):
name: str
email: str
age: int
bio: NotRequired[str] # Optional key (Python 3.11+)
# Type checker validates keys and value types
user: UserDict = {
"name": "Alice",
"email": "[email protected]",
"age": 30,
}
# Errors caught by type checker:
# user["name"] = 42 # Error: expected str
# user["unknown"] = "value" # Error: unknown key
# bad: UserDict = {"name": "Bob"} # Error: missing 'email' and 'age'
# Access is type-safe
name: str = user["name"] # Type checker knows it's str
age: int = user["age"] # Type checker knows it's int
Вложенные TypedDict
from typing import TypedDict
class Address(TypedDict):
street: str
city: str
country: str
class Company(TypedDict):
name: str
address: Address
employees: int
company: Company = {
"name": "Acme Corp",
"address": {
"street": "123 Main St",
"city": "Moscow",
"country": "Russia",
},
"employees": 50,
}
# Type-safe nested access
city: str = company["address"]["city"]
Наследование TypedDict
from typing import TypedDict
class BaseUser(TypedDict):
name: str
email: str
class AdminUser(BaseUser):
role: str
permissions: list[str]
admin: AdminUser = {
"name": "Alice",
"email": "[email protected]",
"role": "superadmin",
"permissions": ["read", "write", "delete"],
}
Literal -- конкретные значения как типы
from typing import Literal
# Only specific values are allowed
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
print(f"Log level set to: {level}")
set_log_level("INFO") # OK
set_log_level("DEBUG") # OK
# set_log_level("TRACE") # Error: not a valid literal
# Literal with other types
Mode = Literal["read", "write", "append"]
def open_file(path: str, mode: Mode = "read") -> str:
return f"Opening {path} in {mode} mode"
# Literal in return type — narrows the type
def get_status(code: int) -> Literal["ok", "error", "pending"]:
if code == 200:
return "ok"
elif code >= 400:
return "error"
return "pending"
Literal с числами и bool
from typing import Literal
# HTTP status codes
HTTPSuccess = Literal[200, 201, 204]
HTTPError = Literal[400, 401, 403, 404, 500]
def handle_response(status: HTTPSuccess | HTTPError) -> str:
match status:
case 200:
return "OK"
case 201:
return "Created"
case 404:
return "Not Found"
case _:
return f"Status: {status}"
Annotated -- метаданные типов
Annotated добавляет метаданные к типам. Используется для валидации (Pydantic), документации и constraints:
from typing import Annotated
# Add metadata to types
type PositiveInt = Annotated[int, "Must be positive"]
type NonEmptyStr = Annotated[str, "Must not be empty"]
type Email = Annotated[str, "Must be a valid email address"]
type Percentage = Annotated[float, "Value between 0 and 100"]
def create_user(
name: NonEmptyStr,
email: Email,
age: PositiveInt,
) -> dict:
return {"name": name, "email": email, "age": age}
# With Pydantic — metadata becomes validation rules
from pydantic import Field
# This is how Pydantic uses Annotated:
# type Age = Annotated[int, Field(ge=0, le=150)]
# type Email = Annotated[str, Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")]
Final -- константы
from typing import Final
# Value cannot be reassigned
MAX_RETRIES: Final = 3
API_BASE_URL: Final[str] = "https://api.example.com"
SUPPORTED_FORMATS: Final[list[str]] = ["json", "csv", "toml"]
# MAX_RETRIES = 5 # Error: cannot assign to final variable
class Config:
MAX_CONNECTIONS: Final = 100
DEFAULT_TIMEOUT: Final[float] = 30.0
ClassVar -- переменные класса
from typing import ClassVar
class Counter:
"""Class with tracked instance count."""
total_instances: ClassVar[int] = 0 # Shared across all instances
name: str # Instance variable
def __init__(self, name: str) -> None:
self.name = name
Counter.total_instances += 1
# ClassVar tells type checkers this is NOT an instance variable
c1 = Counter("first")
c2 = Counter("second")
print(Counter.total_instances) # 2
# c1.total_instances = 5 # Type checker warning
TypeGuard и TypeIs
TypeGuard (Python 3.10+)
from typing import TypeGuard
def is_string_list(items: list[object]) -> TypeGuard[list[str]]:
"""Check if all items are strings."""
return all(isinstance(item, str) for item in items)
def process(items: list[object]) -> None:
if is_string_list(items):
# Type checker now knows items is list[str]
for item in items:
print(item.upper()) # OK — item is str
TypeIs (Python 3.13+)
from typing import TypeIs
def is_int(value: object) -> TypeIs[int]:
"""Narrow type to int."""
return isinstance(value, int)
def process(value: str | int) -> None:
if is_int(value):
print(value + 1) # OK — value is int
else:
print(value.upper()) # OK — value is str
Overload -- перегрузка функций
from typing import overload
@overload
def get_item(index: int) -> str: ...
@overload
def get_item(index: slice) -> list[str]: ...
def get_item(index: int | slice) -> str | list[str]:
"""Get item by index or slice."""
items = ["a", "b", "c", "d"]
return items[index]
# Type checker knows exact return type
single: str = get_item(0) # Returns str
multiple: list[str] = get_item(slice(0, 2)) # Returns list[str]
Практический пример: типобезопасный Event System
from typing import Protocol, TypedDict, Literal
from collections.abc import Callable
from dataclasses import dataclass, field
type EventType = Literal["user.created", "user.deleted", "order.placed"]
class EventData(TypedDict):
event_type: EventType
timestamp: str
payload: dict[str, str | int]
class EventHandler(Protocol):
"""Protocol for event handlers."""
def __call__(self, event: EventData) -> None: ...
@dataclass
class EventBus:
"""Type-safe event bus."""
_handlers: dict[EventType, list[EventHandler]] = field(
default_factory=dict
)
def subscribe(self, event_type: EventType, handler: EventHandler) -> None:
"""Subscribe a handler to an event type."""
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def publish(self, event: EventData) -> None:
"""Publish an event to all subscribers."""
handlers = self._handlers.get(event["event_type"], [])
for handler in handlers:
handler(event)
# Type-safe usage
bus = EventBus()
def on_user_created(event: EventData) -> None:
print(f"User created: {event['payload']}")
bus.subscribe("user.created", on_user_created)
bus.publish({
"event_type": "user.created",
"timestamp": "2024-01-15T10:30:00",
"payload": {"name": "Alice", "age": 30},
})