FFlauron
Python Pro
Безкоштовний урок-прев'ю

Сучасний 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 зберігає тип підкласу.

Тест: перевір себе

  1. Що не так зі старим from typing import List?
  2. У чому різниця dataclass і Pydantic BaseModel?
  3. Що таке Protocol?
  4. Навіщо Final?
  5. Що робить mypy --strict?
Відповіді
  1. Застаріло з Python 3.9 — list[int] нативно. from typing import List залишено для backwards compat. У новому коді — list, dict, tuple, set напряму.
  2. dataclass — генерує __init__/__repr__/__eq__, не валідує. Швидко, легко. Pydantic — валідує типи й constraints на створенні (Field(min_length=1)), серіалізує в JSON. Для DTO, API, ETL — Pydantic. Для внутрішніх value objects — dataclass.
  3. Structural typing. Клас «реалізує» Protocol, якщо має ті самі методи/атрибути, без явного наслідування. Pythonic duck typing з типами. Як interface в Go.
  4. Маркує константу — mypy сповістить, якщо хтось переприсвоїть. Runtime не блокує — це конвенція + статичний аналіз.
  5. Вмикає всі суворі перевірки: усі функції повинні мати hints (disallow_untyped_defs), без Any, перевіряє типи аргументів/повернень/полів. У зрілому проєкті — обов'язково.

Сподобався урок?

Придбайте повний курс, щоб отримати доступ до всіх уроків.

Перейти до курсу