Сучасний Python: type hints, dataclasses, mypy
Python 3.12+ — це вже не «скриптова мова». Type hints, dataclasses, pattern matching, structural subtyping роблять код передбачуваним і читабельним. У 2026 production-Python без типів — не повертайся туди.
Type hints — основа
def greet(name: str, age: int = 0) -> str:
return f"Hello, {name} ({age})"
def parse(raw: str) -> int | None:
try:
return int(raw)
except ValueError:
return None
Hints не впливають на runtime — це підказка для IDE й mypy. Але вони щось ловлять до запуску.
Generics
from collections.abc import Iterable
def first[T](items: Iterable[T]) -> T | None:
for x in items:
return x
return None
x = first([1, 2, 3]) # int
s = first(["a", "b"]) # str
Python 3.12 синтаксис [T] — без TypeVar. Старий стиль:
from typing import TypeVar
T = TypeVar("T")
def first(items: Iterable[T]) -> T | None: ...
Стандартні колекції
def total(prices: list[float]) -> float: ...
def index(items: dict[str, int]) -> int: ...
def pair() -> tuple[int, str]: ...
def options(values: set[str]) -> bool: ...
З Python 3.9 — нативні generics. Не імпортуй List, Dict з typing — застаріле.
Union — X | Y
def parse(raw: str) -> int | float | None: ...
def handle(value: int | str) -> None:
if isinstance(value, int):
print(value + 1)
else:
print(value.upper())
З Python 3.10 — int | None замість Optional[int].
Literal, Final, Annotated
from typing import Literal, Final, Annotated
Mode = Literal["read", "write", "append"]
def open_file(path: str, mode: Mode) -> None: ...
open_file("a.txt", "read") # ✓
open_file("a.txt", "delete") # ✗ mypy впаде
MAX_SIZE: Final = 1024 # не змінювати
Port = Annotated[int, "TCP port 1-65535"]
Protocol — structural subtyping (duck typing з типами)
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None:
print("closing")
def shutdown(c: Closeable) -> None:
c.close()
shutdown(File()) # ✓ — File має close, цього достатньо
Не треба class File(Closeable) — Python перевіряє за структурою. Як Go interfaces.
TypedDict — типізовані словники
from typing import TypedDict, NotRequired
class User(TypedDict):
id: int
name: str
email: NotRequired[str] # опціональне
u: User = {"id": 1, "name": "Olena"}
u["email"] = "o@x.com"
Корисно для JSON-payload без створення dataclass.
dataclasses — value objects з коробки
from dataclasses import dataclass, field
@dataclass
class User:
id: int
name: str
email: str
tags: list[str] = field(default_factory=list)
u = User(1, "Olena", "o@x.com")
print(u) # User(id=1, name='Olena', email='o@x.com', tags=[])
print(u == User(1, "Olena", "o@x.com", [])) # True
Генерує __init__, __repr__, __eq__ за тебе.
frozen — immutable
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
p = Point(1, 2)
p.x = 10 # FrozenInstanceError
slots=True — без __dict__, менше пам'яті.
Методи на dataclass
@dataclass
class Order:
items: list[float]
@property
def total(self) -> float:
return sum(self.items)
def add(self, price: float) -> None:
self.items.append(price)
Pydantic — валідація + типи
dataclass нічого не валідує. Pydantic валідує на старті:
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
id: int
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=0, le=150)
u = User(id=1, name="Olena", email="o@x.com", age=25)
User(id="abc", name="", email="not-email", age=-1)
# pydantic.ValidationError з 4 помилками
У 2026 Pydantic v2 — стандарт для API DTO, конфігурації, ETL.
mypy — статичний аналіз
pip install mypy
mypy myproject/
Конфіг pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
У CI: mypy ловить помилки до запуску. У великих проєктах — обов'язково.
Type narrowing — звуження типів
def handle(value: int | str | None) -> str:
if value is None:
return "empty"
if isinstance(value, int):
return f"int: {value + 1}" # mypy знає, що тут int
return value.upper() # тут str — згенероване
mypy сам відстежує гілки.
Match — pattern matching (3.10+)
def process(event: dict) -> str:
match event:
case {"type": "login", "user": str(u)}:
return f"login: {u}"
case {"type": "purchase", "amount": int(a)} if a > 1000:
return f"big buy: {a}"
case {"type": "purchase", "amount": _}:
return "small buy"
case _:
return "unknown"
Не просто switch — деструктуризація + guard (if).
Self-type (3.11+)
from typing import Self
class Builder:
def with_name(self, n: str) -> Self:
self.name = n
return self
def with_email(self, e: str) -> Self:
self.email = e
return self
Fluent API — Self зберігає тип підкласу.
Тест: перевір себе
- Що не так зі старим
from typing import List? - У чому різниця
dataclassіPydantic BaseModel? - Що таке Protocol?
- Навіщо
Final? - Що робить
mypy --strict?
Відповіді
- Застаріло з Python 3.9 —
list[int]нативно.from typing import Listзалишено для backwards compat. У новому коді —list,dict,tuple,setнапряму. - dataclass — генерує
__init__/__repr__/__eq__, не валідує. Швидко, легко. Pydantic — валідує типи й constraints на створенні (Field(min_length=1)), серіалізує в JSON. Для DTO, API, ETL — Pydantic. Для внутрішніх value objects — dataclass. - Structural typing. Клас «реалізує» Protocol, якщо має ті самі методи/атрибути, без явного наслідування. Pythonic duck typing з типами. Як interface в Go.
- Маркує константу —
mypyсповістить, якщо хтось переприсвоїть. Runtime не блокує — це конвенція + статичний аналіз. - Вмикає всі суворі перевірки: усі функції повинні мати hints (
disallow_untyped_defs), безAny, перевіряє типи аргументів/повернень/полів. У зрілому проєкті — обов'язково.