---
jupytext:
  formats: md:myst
  text_representation:
    extension: .md
    format_name: myst
kernelspec:
  display_name: Python 3.14
  language: python
  name: python3
---

```{code-cell} python
:tags: [remove-cell]

import os, sys
os.environ["PATH"] = f"{os.path.dirname(sys.executable)}:{os.environ['PATH']}"
# uv run по умолчанию берёт venv проекта (course-content/.venv) и ругается,
# если активирован другой (`VIRTUAL_ENV=course-content/lectures/.venv`).
# Убираем переменную — uv берёт проектный venv без предупреждений.
os.environ.pop("VIRTUAL_ENV", None)
```

# Лекция 2. Типы: почему и как

## План

1. **Зачем типы** — что дают и чего не дают.
2. **`collections.abc`** — готовая иерархия абстракций для параметров.
3. **Generics** — `T` как переменная типа.
4. **Protocol** — утиная типизация статически + LSP.
5. **Variance** — когда `list[Dog]` подходит вместо `list[Animal]`.
6. **Forward-references** — ссылки на ещё не определённые типы.
7. **Overload** — точный возвращаемый тип от значения аргумента.
8. **Ещё из typing** — TypedDict, Literal, Annotated, Self, Never, Final,
   `ParamSpec`, `TypeVarTuple`.
9. **Gradual typing** — `Any` vs `object`, включение mypy по одному модулю.

## 1. Зачем типы

Три уровня пользы:

1. **Документация которая не врёт.** Комментарии врут — код меняется, коммент забыли. Аннотации проверяет mypy.
2. **Отлов багов до тестов.** "Передал не то", "забыл про `None`", "поменял сигнатуру" — ловится без запуска.
3. **IDE навигация.** VS Code / PyCharm показывают методы по типу. Без аннотаций — гадают.

**Что типы НЕ делают:** не ускоряют, не заменяют тесты (`int` не гарантирует "неотрицательное"), не делают код читаемее автоматически.

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Термин: static vs runtime check</b>

<ul>
<li><b>Static</b> — проверяет тайпчекер (mypy, pyright) <b>до запуска</b>.</li>
<li><b>Runtime</b> — Python проверяет <b>при выполнении</b> (например, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">isinstance</code>).</li>
</ul>

</div>

Базовые аннотации (`int`, `str`, `list[int]`, `dict[str, int]`, `X | None`, `Callable`) считаем известными из базового курса Даниэля. Идём дальше — в то, что там **не проходили**: Protocol, generics, variance.

<details>
<summary><b>Шпаргалка по базовым аннотациям</b></summary>

```python
from collections.abc import Callable

# переменные
age: int = 30
name: str = "Alice"
active: bool = True

# контейнеры (PEP 585, 3.9+)
nums: list[int] = [1, 2, 3]
mapping: dict[str, int] = {"a": 1}
pair: tuple[str, int] = ("x", 5)
uniq: set[int] = {1, 2}

# опциональность (PEP 604, 3.10+)
maybe_id: int | None = None

# функции
def greet(name: str, times: int = 1) -> str:
    return name * times

# lambda
handler: Callable[[int, str], bool] = lambda n, s: len(s) > n
```

**`typing.List`, `typing.Dict`, `typing.Tuple`, `typing.Set`, `typing.Optional`, `typing.Union` — в новом коде не нужны.** `List`/`Dict`/`Tuple`/`Set` устарели с 3.9 (PEP 585), `Optional`/`Union` формально не устарели — их вытеснил `X | Y` из 3.10 (PEP 604). В новом коде — встроенные `list`, `dict`, `tuple`, `set` и `X | None` / `X | Y`. `ruff` с правилами `UP006`/`UP007`/`UP045` их подсветит и предложит автофикс.

```python
# было (до 3.9)                    ->  стало
from typing import List, Dict, Optional, Union
x: List[int]                       ->  x: list[int]
d: Dict[str, int]                  ->  d: dict[str, int]
maybe: Optional[str]               ->  maybe: str | None
either: Union[int, str]            ->  either: int | str
```

</details>

---

## 2. Абстракции из `collections.abc`

### Мотивация

Написали функцию:

```{code-cell} python
%%writefile /tmp/abc_motivation.py
def total(items: list[int]) -> int:
    return sum(items)


total([1, 2, 3])            # ok
total((1, 2, 3))            # mypy: tuple вместо list
total(range(10))            # mypy: range вместо list
total(x for x in [1, 2])    # mypy: generator вместо list
```

```{code-cell} python
!uv run mypy --strict /tmp/abc_motivation.py || true
```

В рантайме всё работает: `sum` умеет итерироваться по чему угодно. А тайпчекер режет — параметр объявлен как `list[int]`. Хочется общее имя для "что-то, по чему можно проитерироваться". Оно уже есть в stdlib.

### Иерархия `collections.abc`

![Иерархия collections.abc](img/collections-abc.svg)

Каждый прямоугольник = один абстрактный класс. Внутри — его контракт: dunder-методы, которые наследник пишет сам, и то, что ABC даёт бесплатно (`keys`, `get`, `index`, `count`). Стрелки — наследование. А внизу связь другая: встроенные типы ABC **не наследуют** — `MutableMapping` в `dict.__mro__` не найти, — но `issubclass(dict, MutableMapping)` всё равно `True`. Класс можно объявить подклассом ABC задним числом, не трогая его код: так сделали со встроенными типами, так же можно подтянуть и чужой класс из библиотеки, которая про ваш ABC не знает. Работает это через отдельный механизм, раскроем его чуть позже в курсе.

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Термин: MRO (method resolution order)</b>

Порядок, в котором Python ищет атрибут по цепочке базовых классов. Лежит в <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Cls.&#95;&#95;mro&#95;&#95;</code> — кортеж классов от самого класса до <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">object</code>:

<ul>
<li><code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">dict.&#95;&#95;mro&#95;&#95;</code> -> <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">(dict, object)</code></li>
<li><code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">bool.&#95;&#95;mro&#95;&#95;</code> -> <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">(bool, int, object)</code></li>
</ul>

Это и есть "настоящее" наследование. Всё, чего в <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">&#95;&#95;mro&#95;&#95;</code> нет, наследованием не является — даже если <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">issubclass</code> говорит <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">True</code>.

</div>

Самые простые ABC — по одному методу:

- `Sized` — `__len__`
- `Iterable[T]` — `__iter__`
- `Container[T]` — `__contains__`
- `Reversible[T]` — `__reversed__` (наследник `Iterable`, поэтому ещё и `__iter__`)

`Collection[T]` = Sized + Iterable + Container. `Mapping`/`Set`/`Sequence` — три контракта на чтение, `Mutable`-версии добавляют запись.

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Наследоваться необязательно</b>

Свой класс с <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">&#95;&#95;contains&#95;&#95;</code> уже считается <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Container</code>, наследовать не нужно — <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Container</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Sized</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Iterable</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Reversible</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Collection</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Hashable</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Callable</code> устроены так, что <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">isinstance</code> смотрит на наличие метода. А <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Mapping</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Sequence</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Set</code> — нет, одних методов им мало, нужно наследование.

</div>

```{code-cell} python
from collections.abc import Container


class Range:
    def __init__(self, lo: int, hi: int) -> None:
        self.lo, self.hi = lo, hi

    def __contains__(self, x: object) -> bool:
        return isinstance(x, int) and self.lo <= x <= self.hi


r = Range(1, 10)
print(5 in r)                        # True — работает без наследования
print(isinstance(r, Container))      # True — метод есть, значит Container
```

Наследовать явно (`class Range(Container): ...`) стоит когда нужны бесплатные mixin-методы — например `Sequence` наследнику даёт `__iter__`, `__reversed__`, `index`, `count` "в подарок" из `__getitem__` + `__len__`. Для `Container` бонусов нет — можно не наследовать.

<details>
<summary><b>Напоминание: как выглядят dunder-методы</b></summary>

Ниже — минимальные реализации, чтобы освежить память после базового курса.

```python
class MyBag:
    def __init__(self, items):
        self._items = list(items)

    # для Sized — len(x)
    def __len__(self) -> int:
        return len(self._items)

    # для Iterable — for x in obj / list(obj) / sum(obj)
    def __iter__(self):
        yield from self._items

    # для Container — x in obj
    def __contains__(self, x: object) -> bool:
        return x in self._items

    # для Reversible — reversed(obj)
    def __reversed__(self):
        yield from reversed(self._items)

    # для Sequence — obj[i], obj[a:b]
    def __getitem__(self, i):
        return self._items[i]

    # для MutableSequence — obj[i] = v
    def __setitem__(self, i, v) -> None:
        self._items[i] = v

    # для MutableSequence — del obj[i]
    def __delitem__(self, i) -> None:
        del self._items[i]

    # для сравнения — obj == other
    def __eq__(self, other: object) -> bool:
        return isinstance(other, MyBag) and self._items == other._items

    # для использования в set / как ключа dict — hash(obj)
    def __hash__(self) -> int:
        return hash(tuple(self._items))
```

Всё что делает ABC — проверяет наличие этих методов и/или подкидывает бесплатные, вычисляемые из них (`index`, `count`, `keys`, `items`, `values`, ...).

</details>

### Использование

Правило: **типизируй параметр по минимуму методов, которые реально нужны**. Тогда функция примет максимум разных объектов.

`Iterable[int]` — просто "что-то, что можно проитерировать":

```{code-cell} python
%%writefile /tmp/iterable_ex.py
from collections.abc import Iterable


def total(items: Iterable[int]) -> int:
    return sum(items)


total([1, 2, 3])          # list
total((1, 2, 3))          # tuple
total(range(10))          # range
total(x for x in [1, 2])  # generator
```

```{code-cell} python
!uv run mypy --strict /tmp/iterable_ex.py
```

`Sized` — только "у него есть длина":

```{code-cell} python
%%writefile /tmp/sized_ex.py
from collections.abc import Sized


def is_empty(x: Sized) -> bool:
    return len(x) == 0


is_empty([])       # list
is_empty("")       # str
is_empty({1, 2})   # set
```

```{code-cell} python
!uv run mypy --strict /tmp/sized_ex.py
```

`Sequence[T]` — можно индексировать по `int` и есть длина. Строже чем `Iterable`, но принимает `list`, `tuple`, `str`, `range`:

```{code-cell} python
%%writefile /tmp/sequence_ex.py
from collections.abc import Sequence


def second(items: Sequence[int]) -> int:
    return items[1]        # для Iterable — так нельзя, нет __getitem__


second([1, 2, 3])
second((10, 20, 30))
second(range(5, 10))
```

```{code-cell} python
!uv run mypy --strict /tmp/sequence_ex.py
```

`Mapping[K, V]` — словарь только для чтения. Принимает `dict`, `collections.OrderedDict` и свои классы, **унаследованные** от `Mapping`: в отличие от `Iterable`/`Sized`, одних методов тут мало.

```{code-cell} python
%%writefile /tmp/mapping_ex.py
from collections.abc import Mapping


def get_name(users: Mapping[int, str], uid: int) -> str:
    return users.get(uid, "unknown")


get_name({1: "Alice", 2: "Bob"}, 1)
```

```{code-cell} python
!uv run mypy --strict /tmp/mapping_ex.py
```

Обратно: чем **уже** параметр, тем меньше вызывающих подходит. `list[int]` не примет `tuple`, `Iterable[int]` — примет.

Шпаргалка:

| Что делаешь с параметром | Тип |
|---|---|
| Только читаешь длину | `Sized` |
| Один раз итерируешь | `Iterable[T]` |
| Итерируешь несколько раз | `Collection[T]` (или `Iterable[T]` + скопировать в `list`) |
| Индексируешь по `int`, длина | `Sequence[T]` |
| Индексируешь по ключу | `Mapping[K, V]` |
| Модифицируешь | `MutableSequence[T]` / `MutableMapping[K, V]` / `MutableSet[T]` |

<div class="adm-warn" style="background: #FFBA00; border-left: 5px solid #ffcc00; color: #222; padding: 15px; margin: 10px 0;">

<b>Раньше это было в <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing</code></b>

До Python 3.9 generic-версий у <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">list</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">dict</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">tuple</code> не было — нельзя было написать <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">list&#91;int&#93;</code>. И у ABC тоже: <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">collections.abc.Iterable</code> был обычным ABC без параметров типа. Поэтому в <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing</code> держали параметризованные копии: <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing.List</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing.Dict</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing.Iterable</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing.Sequence</code>, <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing.Callable</code> — единственный способ написать <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">Iterable&#91;int&#93;</code>.

С 3.9 (<a href="https://peps.python.org/pep-0585/" style="color: #003366; text-decoration: underline;">PEP 585</a>) generic-синтаксис работает у встроенных типов и у ABC напрямую. Теперь:

<pre style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">
# было (до 3.9)                           ->  стало
from typing import Iterable, Sequence     ->  from collections.abc import Iterable, Sequence
from typing import List, Dict, Callable   ->  list, dict, from collections.abc import Callable
</pre>

Всё что в <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">typing</code> с этими именами — устаревшие алиасы, оставлены для обратной совместимости. В новом коде — <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">collections.abc</code>. <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">ruff</code> с правилом <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">UP035</code> подсветит и предложит автофикс.

</div>

---

## 3. Generics: `T` как переменная типа

*Одна функция работает для `list[int]` и для `list[str]`. Как это записать в типах?*

```{code-cell} python
def first(items):
    return items[0]


print(first([1, 2, 3]))    # int
print(first(["a", "b"]))   # str
```

```{code-cell} python
%%writefile /tmp/generic_basic.py
def first[T](items: list[T]) -> T:
    return items[0]


x: int = first([1, 2, 3])   # T = int
y: str = first(["a", "b"])  # T = str
```

```{code-cell} python
!uv run mypy --strict /tmp/generic_basic.py
```

Синтаксис `[T]` — [PEP 695](https://peps.python.org/pep-0695/) (3.12+). А раньше:

```{code-cell} python
%%writefile /tmp/generic_old.py
from typing import TypeVar

T = TypeVar("T")


def first(items: list[T]) -> T:
    return items[0]
```

```{code-cell} python
!uv run mypy --strict /tmp/generic_old.py
```

**Алиасы типов** (type aliases) — тот же PEP 695:

```{code-cell} python
%%writefile /tmp/type_alias.py
type UserId = int
type UserMap = dict[UserId, str]


def load(users: UserMap) -> UserId | None:
    for uid in users:
        return uid
    return None
```

```{code-cell} python
!uv run mypy --strict /tmp/type_alias.py
```

`type X = Y` — **ленивый**: правая часть вычисляется не сразу, а при первом обращении к значению алиаса (`X.__value__`). Значит `X` может ссылаться на имя, определённое ниже по файлу (это называется **forward reference**) — или на самого себя, то есть быть рекурсивным:

```{code-cell} python
type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | None

sample: JSON = {"a": [1, 2, {"b": None}]}
print(sample)
```

### А как было до PEP 695

**Попытка 1: обычное присваивание** — правая часть вычисляется сразу:

```{code-cell} python
:tags: [raises-exception]

Nodes = list[TreeNode]   # TreeNode ещё не определён


class TreeNode:
    children: Nodes
```

`NameError` — правая часть `list[TreeNode]` пытается зарезолвить `TreeNode` немедленно, а класса ещё нет.

**Попытка 2: строка внутри правой части** — тайпчекер видит "TreeNode" как forward-ref, рантайм — как обычную строку, не пытается её резолвить:

```{code-cell} python
%%writefile /tmp/alias_string.py
Nodes = list["TreeNode"]  # "TreeNode" в кавычках — forward-ref строкой


class TreeNode:
    children: Nodes
```

```{code-cell} python
!uv run mypy --strict /tmp/alias_string.py
```

В двусмысленных случаях алиас помечали явно через `TypeAlias` ([PEP 613](https://peps.python.org/pep-0613/)) — тогда mypy не путал его с обычной переменной:

```{code-cell} python
%%writefile /tmp/alias_typealias.py
from typing import TypeAlias

Nodes: TypeAlias = list["TreeNode"]  # явно: это алиас, а не runtime-значение


class TreeNode:
    children: Nodes
```

```{code-cell} python
!uv run mypy --strict /tmp/alias_typealias.py
```

В сумме, у второй попытки:

- forward-ref только через строку — легко опечататься, тайпчекер найдёт (если запустишь), рантайм узнает поздно;
- рекурсивные типы (`JSON = dict[str, "JSON"] | ...`) без гимнастики со строками не выразить;
- `TypeAlias` — ещё одна конструкция и ещё один импорт, которые надо помнить.

**PEP 695** решает всё сразу — ленивость на уровне языка, без строк, без импорта:

```python
type Nodes = list[TreeNode]        # ok, TreeNode может быть ниже
type JSON = dict[str, JSON] | ...  # ok, рекурсия работает
```

---

## 4. Protocol и утиная типизация статически

Как выразить "любой объект с методом `close()`"? Наследование от общей базы не работает — объект может быть из чужой библиотеки.

### Утиная и гусиная типизация — напоминание

У Даниэля разбирали два подхода:

- **Утиная типизация** — "ходит как утка, крякает как утка -> утка". Смотрим только на **наличие методов**, никаких `isinstance` и наследований. Проверка в runtime — упадёт если метод не нашёлся.
- **Гусиная типизация** — класс **сам заявляет**, что реализует интерфейс: наследуется от `abc.ABC`, а `isinstance` проверяет эту пометку.

`typing.Protocol` — это **утиная типизация статически**: mypy смотрит на форму (какие методы есть), но проверяет до запуска.

![Гусиная (ABC) vs Утиная (Protocol)](img/nominal-vs-structural.svg)

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Академические названия</b>

В доках mypy, PEP и учебниках то же самое зовут <b>nominal typing</b> (гусиная, по имени/наследованию) и <b>structural typing</b> (утиная, по структуре). Дальше в лекции — "гусиная"/"утиная", но в документации вы встретите именно эту пару.

</div>

### ABC vs Protocol

```{code-cell} python
%%writefile /tmp/abc_example.py
from abc import ABC, abstractmethod


class Closeable(ABC):
    @abstractmethod
    def close(self) -> None: ...


class File(Closeable):  # обязательно наследуем
    def close(self) -> None: ...


class Socket:  # НЕ наследуем
    def close(self) -> None: ...


def cleanup(x: Closeable) -> None:
    x.close()


cleanup(File())    # ok
cleanup(Socket())  # error — Socket не наследуется от Closeable
```

```{code-cell} python
!uv run mypy --strict /tmp/abc_example.py || true
```

```{code-cell} python
%%writefile /tmp/protocol_example.py
from typing import Protocol


class Closeable(Protocol):
    def close(self) -> None: ...


class File:
    def close(self) -> None: ...


class Socket:
    def close(self) -> None: ...


def cleanup(x: Closeable) -> None:
    x.close()


cleanup(File())    # ok
cleanup(Socket())  # ok — есть .close(), этого достаточно
```

```{code-cell} python
!uv run mypy --strict /tmp/protocol_example.py
```

**Protocol для чужого, ABC для своего.**

### runtime_checkable

По умолчанию Protocol — только static. Для `isinstance` нужен декоратор:

```{code-cell} python
from typing import Protocol, runtime_checkable


@runtime_checkable
class Closeable(Protocol):
    def close(self) -> None: ...


class File:
    def close(self) -> None: ...


print(isinstance(File(), Closeable))    # True
print(isinstance("hello", Closeable))   # False
```

Проверяет **только наличие методов**, не сигнатуры. Медленно для больших Protocol'ов.

### Liskov substitution: как ломается наследование

**LSP** — это **L** из **SOLID**, набора из пяти принципов ООП-дизайна ([Роберт Мартин, 2000-е](https://ru.wikipedia.org/wiki/SOLID_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5))). В контексте типов интересен именно L — остальные под катом.

<details>
<summary><b>SOLID полностью</b></summary>

- **S — Single Responsibility Principle.** Класс должен иметь одну причину для изменения. Один класс = одна ответственность.
- **O — Open/Closed Principle.** Код открыт для расширения (новые фичи через новые классы/функции), закрыт для модификации (не переписываем существующее).
- **L — Liskov Substitution Principle.** Подкласс должен быть подставим вместо родителя без нарушения работы клиента. Разбираем ниже.
- **I — Interface Segregation Principle.** Много маленьких протоколов лучше одного большого — не заставляем клиента зависеть от методов, которые он не использует. У нас в §4 это "Protocol как минимальный контракт".
- **D — Dependency Inversion Principle.** Высокоуровневые модули не зависят от низкоуровневых напрямую — оба зависят от абстракции (Protocol / ABC). В Python — тайпнули параметр как `Protocol`, и подставится любая реализация.

</details>

**LSP** (Барбара Лисков, 1987): подкласс должен быть **взаимозаменяем** (substitutable) с родителем — везде, где ожидается объект родителя, должен подойти объект подкласса без сюрпризов для клиента.

![LSP: клиент, база, подкласс](img/lsp.svg)

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

**1. Возвращаемое значение: подкласс возвращает шире -> плохо.**

```{code-cell} python
%%writefile /tmp/lsp_return.py
class Animal: ...
class Dog(Animal): ...


class Factory:
    def create(self) -> Dog:
        return Dog()


class BadFactory(Factory):
    def create(self) -> Animal:  # Animal шире Dog — клиент ждёт Dog
        return Animal()
```

```{code-cell} python
!uv run mypy --strict /tmp/lsp_return.py || true
```

**2. Аргумент: подкласс принимает уже -> плохо.**

```{code-cell} python
%%writefile /tmp/lsp_args.py
class Animal: ...
class Dog(Animal): ...


class Handler:
    def handle(self, x: Animal) -> None: ...


class BadHandler(Handler):
    def handle(self, x: Dog) -> None: ...  # Dog уже Animal
```

```{code-cell} python
!uv run mypy --strict /tmp/lsp_args.py || true
```

**3. Добавили обязательный аргумент -> плохо.**

```{code-cell} python
%%writefile /tmp/lsp_arity.py
class Base:
    def process(self, item: int) -> None: ...


class Bad(Base):
    def process(self, item: int, priority: int) -> None: ...
```

```{code-cell} python
!uv run mypy --strict /tmp/lsp_arity.py || true
```

Общее правило: **возвращать можно уже, принимать — шире**. Дальше увидим ту же логику в generic'ах.

---

## 5. Variance: `list[Dog]` vs `list[Animal]`

Знаем: `Dog` подкласс `Animal`. А `list[Dog]` — это `list[Animal]`?

Ответ зависит от того, что тип **делает** с `T`.

![Covariance / Contravariance / Invariance](img/variance.svg)

**Кратко:**
- Только **producer** (выдаёт `T` наружу) — **covariant**, движется в ту же сторону что `Dog <: Animal`.
- Только **consumer** (принимает `T` внутрь) — **contravariant**, в обратную.
- И то и то — **invariant**, ни туда ни сюда.

### list — invariant

```{code-cell} python
%%writefile /tmp/variance_list.py
class Animal: ...
class Dog(Animal):
    def bark(self) -> str: return "woof"
class Cat(Animal): ...


def add_cat(animals: list[Animal]) -> None:
    animals.append(Cat())  # если бы list был covariant — сюда пролез бы Cat


def first_bark(dogs: list[Dog]) -> str:
    return dogs[0].bark()  # ждём Dog — вызываем .bark()


dogs: list[Dog] = [Dog()]
add_cat(dogs)          # если бы list[Dog] <: list[Animal] — прошло бы
first_bark(dogs)       # но тогда в runtime внутри мог оказаться Cat, и .bark() упадёт


animals: list[Animal] = [Cat(), Cat()]
first_bark(animals)    # обратно тоже нельзя: если бы list[Animal] <: list[Dog] —
                       # прошло бы, а внутри .bark() у Cat нет
```

```{code-cell} python
!uv run mypy --strict /tmp/variance_list.py || true
```

Mypy правильно ругается — `list` invariant.

### Sequence — covariant

Формально: `Sequence[Dog] <: Sequence[Animal]`. Стрелка **сохраняется** относительно `Dog <: Animal` — потому и "**co**variance".

Почему: `Sequence[Dog]` умеет выдавать только `Dog`, а `Dog` — это и есть `Animal`. Функция, ждущая `Sequence[Animal]`, будет только читать элементы и обращаться с ними как с `Animal` — `Dog` подходит.

```{code-cell} python
%%writefile /tmp/variance_sequence.py
from collections.abc import Sequence


class Animal:
    def speak(self) -> str: return "..."
class Dog(Animal): ...


def describe_first(animals: Sequence[Animal]) -> str:
    return animals[0].speak()   # индексируем и читаем — Animal-контракта хватает


dogs: list[Dog] = [Dog(), Dog()]
describe_first(dogs)   # ok — Sequence только читает, Dog <: Animal
```

```{code-cell} python
!uv run mypy --strict /tmp/variance_sequence.py
```

### Pushable — contravariant

Формально: `Pushable[Animal] <: Pushable[Dog]`. Стрелка **разворачивается** относительно `Dog <: Animal` — потому и "**contra**variance".

Почему: `Pushable[Animal]` умеет глотать любое `Animal` (в том числе `Dog`). Если функция ждёт `Pushable[Dog]` — то есть будет пихать туда только `Dog`, — `Pushable[Animal]` подходит с запасом.

```{code-cell} python
%%writefile /tmp/variance_consumer.py
from typing import Protocol, TypeVar

T_contra = TypeVar("T_contra", contravariant=True)


class Animal: ...
class Dog(Animal): ...


class Pushable(Protocol[T_contra]):
    def push(self, item: T_contra) -> None: ...


class AnimalLog:
    def push(self, item: Animal) -> None: ...


class DogLog:
    def push(self, item: Dog) -> None: ...


def dump_dogs(sink: Pushable[Dog]) -> None:
    sink.push(Dog())


# AnimalLog глотает любое Animal — Dog для него тем более ok
dump_dogs(AnimalLog())   # ok — contravariance
dump_dogs(DogLog())      # ok — точное совпадение
```

```{code-cell} python
!uv run mypy --strict /tmp/variance_consumer.py
```

Здесь мы **явно** объявили TypeVar contravariant через `contravariant=True` и суффикс `_contra` в имени — самодокументирующий контракт "этот протокол только принимает T внутрь". PEP 695 (`class Pushable[T](Protocol)`) тоже работает — mypy выведет contravariance сам из того что `T` только в аргументе метода. Но вывод типов молчит: контракт виден только из тела, из объявления — нет. В API-коде явное лучше.

### Callable — обе стороны

`Callable[[Arg], Ret]` — generic по двум позициям с **разной** variance:

- **Возвращаемый тип** — covariant (producer).
- **Аргумент** — contravariant (consumer).

```{code-cell} python
%%writefile /tmp/callable_variance.py
from collections.abc import Callable


class Animal: ...
class Dog(Animal): ...


def take_animal(a: Animal) -> None: ...
def take_dog(d: Dog) -> None: ...


# Функция, принимающая шире (Animal), — годится там где ждут функцию для Dog.
handler_ok: Callable[[Dog], None] = take_animal    # ok

# Обратное — нельзя.
handler_bad: Callable[[Animal], None] = take_dog   # error
```

```{code-cell} python
!uv run mypy --strict /tmp/callable_variance.py || true
```

Это ровно та же LSP-логика из §4, только на generic'ах.

### `type[T]` — сам класс, не экземпляр

![type[T] vs instance](img/type-hierarchy.svg)

```{code-cell} python
%%writefile /tmp/type_t.py
class Animal: ...
class Dog(Animal): ...


def make(cls: type[Animal]) -> Animal:
    return cls()


make(Dog)   # ok — Dog это type[Animal]
make(int)   # error — int не подкласс Animal
```

```{code-cell} python
!uv run mypy --strict /tmp/type_t.py || true
```

Живой пример — фабрики / реестры: функция принимает `cls: type[Base]` и возвращает `cls()`.

### Bounded TypeVar

Хочется generic только для сравнимых типов. Наивная попытка — обычный класс `Comparable` как bound:

```{code-cell} python
%%writefile /tmp/bounded_nominal.py
class Comparable:
    def __lt__(self, other: Comparable, /) -> bool:
        raise NotImplementedError


def sort_pair[T: Comparable](a: T, b: T) -> tuple[T, T]:
    return (a, b) if a < b else (b, a)


sort_pair(1, 2)      # int не наследуется от Comparable
sort_pair("a", "b")  # str не наследуется от Comparable
```

```{code-cell} python
!uv run mypy --strict /tmp/bounded_nominal.py || true
```

Mypy режет: `int` и `str` формально не подклассы нашего `Comparable`, хотя `__lt__` у них есть. Это **гусиная типизация** — проверка по имени/наследованию. Для встроенных типов не работает.

Решение — сделать `Comparable` **Protocol**. Тогда mypy проверит по форме (наличию `__lt__`), а не по наследованию:

```{code-cell} python
%%writefile /tmp/bounded_protocol.py
from typing import Protocol, Self


class Comparable(Protocol):
    def __lt__(self, other: Self, /) -> bool: ...


def sort_pair[T: Comparable](a: T, b: T) -> tuple[T, T]:
    return (a, b) if a < b else (b, a)


sort_pair(1, 2)      # у int есть __lt__ — подходит
sort_pair("a", "b")  # у str есть __lt__ — подходит
```

```{code-cell} python
!uv run mypy --strict /tmp/bounded_protocol.py
```

Правило: bounded TypeVar, который планируешь применять к встроенным / чужим типам, — почти всегда `Protocol`.

---

## 6. Forward-references (кратко)

Ссылка на класс, который ещё не определён:

```{code-cell} python
class Node:
    def __init__(self, value: int, next: Node | None = None) -> None:
        self.value = value
        self.next = next


print(Node.__init__.__annotations__)
```

На **3.14** это работает: [PEP 649](https://peps.python.org/pep-0649/) — аннотации ленивы, резолвятся при первом обращении к `__annotations__`. На 3.13 и раньше — падает `NameError`, надо было либо кавычить (`"Node | None"`), либо ставить `from __future__ import annotations` (PEP 563).

Практический вывод: в **аннотациях** на 3.14 — забудь про кавычки и `from __future__ import annotations`, всё работает.

---

## 7. Overload

Возврат зависит от **значения** аргумента, не типа. Типичное — `open("f", "r")` -> `TextIO`, `open("f", "rb")` -> `BinaryIO`. Union плох: клиент получит `TextIO | BinaryIO` и вынужден делать `isinstance`.

Ниже используется `Literal["r"]` — тип, разрешающий ровно одно конкретное значение (строку `"r"`, никакую другую). Разберём его подробно в следующем разделе, здесь достаточно понимать, что "передал не эту строку — тайпчекер отругал".

```{code-cell} python
%%writefile /tmp/overload_open.py
from typing import Literal, TextIO, BinaryIO, overload


@overload
def my_open(path: str, mode: Literal["r"]) -> TextIO: ...
@overload
def my_open(path: str, mode: Literal["rb"]) -> BinaryIO: ...


def my_open(path: str, mode: str) -> TextIO | BinaryIO:  # реализация
    if mode == "r":
        return open(path)
    return open(path, "rb")


text = my_open("a.txt", "r")     # mypy знает: TextIO
binary = my_open("a.bin", "rb")  # mypy знает: BinaryIO
```

```{code-cell} python
!uv run mypy --strict /tmp/overload_open.py
```

- Заглушки `@overload` — только `...`, без тела.
- Реализация — последняя, без `@overload`.
- Порядок важен: mypy берёт первый матч.

---

## 8. Ещё из typing

### TypedDict — типы для dict'ов

JSON пришёл в dict, ключи известны. Заменяем `dict[str, Any]`.

```{code-cell} python
%%writefile /tmp/td_typed.py
from typing import TypedDict


class User(TypedDict):
    id: int
    name: str
    email: str


def get_name(user: User) -> str:
    name: str = user["nam"]  # error: нет ключа "nam"
    return name
```

```{code-cell} python
!uv run mypy --strict /tmp/td_typed.py || true
```

Опциональные поля — [PEP 655](https://peps.python.org/pep-0655/):

```{code-cell} python
%%writefile /tmp/td_required.py
from typing import TypedDict, NotRequired


class User(TypedDict):
    id: int
    name: str
    email: NotRequired[str]  # может отсутствовать


ok: User = {"id": 1, "name": "Alice"}    # email класть необязательно
bad: User = {"id": 2, "email": "a@b.c"}  # error — обязательный "name" пропущен


def send(u: User) -> None:
    print(u["email"])            # mypy молчит, а в runtime это KeyError


def send_ok(u: User) -> None:
    print(u.get("email", "-"))   # безопасно
```

```{code-cell} python
!uv run mypy --strict /tmp/td_required.py || true
```

Ругается только на конструирование: пропущенный обязательный ключ — ошибка, отсутствующий `NotRequired` — нет. А вот **чтение** `u["email"]` mypy не проверяет вообще: наличие опциональных ключей он не отслеживает. Поэтому `.get()` с дефолтом.

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Термин: сужение типа (type narrowing)</b>

Mypy сужает тип после проверки: <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">isinstance(x, int)</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">if x is not None</code>, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">match</code>. Внутри ветки тип становится уже.

</div>

```{code-cell} python
%%writefile /tmp/narrowing.py
from typing import TypedDict, NotRequired, reveal_type


class User(TypedDict):
    id: int
    email: NotRequired[str]


def f(x: int | None) -> None:
    if x is not None:
        reveal_type(x)   # сузилось


def g(u: User) -> None:
    if "email" in u:
        reveal_type(u)   # не сузилось
```

```{code-cell} python
!uv run mypy --strict /tmp/narrowing.py
```

`int | None` стало `int`, а TypedDict остался с тем же `email?` — проверка `in` у него ничего не сужает.

<div class="adm-warn" style="background: #FFBA00; border-left: 5px solid #ffcc00; color: #222; padding: 15px; margin: 10px 0;">

<b><code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">NotRequired&#91;str&#93;</code> vs <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">str &#124; None</code></b>

<ul>
<li><code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">email: NotRequired&#91;str&#93;</code> — ключа может не быть в dict.</li>
<li><code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">email: str &#124; None</code> — ключ есть, значение может быть <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">None</code>.</li>
</ul>

TypedDict — <b>чисто информация для тайпчекера</b>. В рантайме это обычный dict, никаких проверок (что <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">id</code> действительно int, что <code style="background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px;">name</code> действительно str).

</div>

### Literal — конкретные значения как типы

[PEP 586](https://peps.python.org/pep-0586/). Не "строка", а строго `"r" | "rb"`.

```{code-cell} python
%%writefile /tmp/literal_basic.py
from typing import Literal

Mode = Literal["r", "rb", "w", "wb"]


def do_open(path: str, mode: Mode) -> None: ...


do_open("f.txt", "r")   # ok
do_open("f.txt", "rw")  # error: "rw" не в Literal
```

```{code-cell} python
!uv run mypy --strict /tmp/literal_basic.py || true
```

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Literal vs Enum</b>

Как только вариантов больше 2-3 или хочется автодополнения / рефакторинга — лучше <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">enum.Enum</code>:

<pre style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">
from enum import Enum

class Mode(Enum):
    READ = "r"
    READ_BINARY = "rb"
    WRITE = "w"
    WRITE_BINARY = "wb"

def do_open(path: str, mode: Mode) -> None: ...

do_open("f.txt", Mode.READ)
</pre>

Плюсы Enum: IDE подставит варианты по <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Mode.</code>, переименование значения — одно место, легко добавить методы (<code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">is&#95;binary()</code>), в рантайме — настоящий объект (можно передать между модулями, сравнить через <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">is</code>).

<b>Literal</b> остаётся хорош для флагов-паттернов, приходящих извне как есть (JSON, CLI, <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">open(mode="r")</code> в stdlib), где enum-обёртка была бы лишней.

</div>

### Annotated — метаданные к типу

[PEP 593](https://peps.python.org/pep-0593/). Прицепить к типу произвольный объект. Для mypy — исходный тип, для рантайм-фреймворков — источник конфигурации.

```{code-cell} python
from typing import Annotated, get_type_hints


class Positive:
    def __init__(self, min_value: int = 0) -> None:
        self.min_value = min_value


Age = Annotated[int, Positive(min_value=0)]


def greet(age: Age) -> str:
    return f"you are {age}"


# для mypy Age это int
greet(30)

# метаданные — через get_type_hints с include_extras=True
hints = get_type_hints(greet, include_extras=True)
print(hints["age"].__metadata__)
```

Так работают:
- **pydantic v2** — `Annotated[int, Field(gt=0)]`
- **FastAPI** — `Annotated[Session, Depends(get_db)]`
- **msgspec** — `Annotated[int, msgspec.Meta(ge=0)]`
- **typer** — `Annotated[int, typer.Option("--port")]`

### Self — цепочки методов через `.`

[PEP 673](https://peps.python.org/pep-0673/).

```{code-cell} python
%%writefile /tmp/self_builder.py
from typing import Self


class Query:
    def __init__(self) -> None:
        self._where: list[str] = []

    def where(self, cond: str) -> Self:
        self._where.append(cond)
        return self


class UserQuery(Query):
    def active(self) -> Self:
        return self.where("active = true")


# .where() у UserQuery возвращает UserQuery, не Query — цепочка не теряет тип
q: UserQuery = UserQuery().where("age > 18").active()
```

```{code-cell} python
!uv run mypy --strict /tmp/self_builder.py
```

Без `Self` пришлось бы писать `TypeVar("T", bound="Query")` — с кавычками, потому что `TypeVar` объявляется до класса и вычисляет `bound` сразу, а имени `Query` в этот момент ещё нет.

### Never / assert_never — exhaustive match

[PEP 484](https://peps.python.org/pep-0484/) даёт `NoReturn`. `Never` и `assert_never` добавили в 3.11 — для проверки полноты `match/case`.

```{code-cell} python
%%writefile /tmp/never_match.py
from typing import Literal, assert_never

Colour = Literal["red", "green", "blue"]


def rgb(c: Colour) -> tuple[int, int, int]:
    match c:
        case "red":
            return (255, 0, 0)
        case "green":
            return (0, 255, 0)
        # забыли "blue"
        case _:
            assert_never(c)  # error: "blue" не покрыт
```

```{code-cell} python
!uv run mypy --strict /tmp/never_match.py || true
```

Пропустил кейс — mypy покажет какой именно.

### Final — константы

[PEP 591](https://peps.python.org/pep-0591/). Запрет переприсваивания.

```{code-cell} python
%%writefile /tmp/final_const.py
from typing import Final

MAX_RETRIES: Final = 3


def retry() -> None:
    global MAX_RETRIES
    MAX_RETRIES = 5  # mypy: error, runtime: спокойно переприсвоится


retry()
print(MAX_RETRIES)
```

```{code-cell} python
!uv run mypy --strict /tmp/final_const.py || true
```

А в рантайме всё работает — Python про `Final` ничего не знает:

```{code-cell} python
!uv run python /tmp/final_const.py
```

`@final` на классе — запрет наследования, на методе — запрет переопределения:

```{code-cell} python
%%writefile /tmp/final_class.py
from typing import final


class Base:
    @final
    def critical(self) -> None: ...


class Sub(Base):
    def critical(self) -> None: ...  # error: cannot override final method
```

```{code-cell} python
!uv run mypy --strict /tmp/final_class.py || true
```

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Final vs UPPER_CASE</b>

В Python нет настоящих констант — <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">MAX&#95;RETRIES = 3</code> можно переприсвоить. UPPER_CASE — только конвенция. <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Final</code> — реальная проверка mypy. Для настоящей неизменяемости в runtime — <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">frozen dataclass</code> / <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">tuple</code>.

</div>

### `ParamSpec` — типизируем декораторы

Как типизировать `@logged`, который **сохраняет** сигнатуру
декорируемой функции?

```{code-cell} python
%%writefile /tmp/paramspec_demo.py
from collections.abc import Callable
from functools import wraps
from typing import TYPE_CHECKING

def logged[**P, R](f: Callable[P, R]) -> Callable[P, R]:
    @wraps(f)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"calling {f.__name__}({args}, {kwargs})")
        return f(*args, **kwargs)
    return wrapper

@logged
def greet(name: str, age: int = 30) -> str:
    return f"hello {name}, {age}"

print(greet("Ivan", age=25))

if TYPE_CHECKING:
    reveal_type(greet)
# greet(123)  # mypy сообщит, что ожидается str
```

```{code-cell} python
!mypy --strict /tmp/paramspec_demo.py
!python /tmp/paramspec_demo.py
```

### `TypeVarTuple` — набор типов произвольной длины

`TypeVarTuple` описывает набор типов произвольной длины. Можно выделить первый
аргумент функции и сохранить типы всех остальных:

```{code-cell} python
%%writefile /tmp/typevartuple_demo.py
from typing import TYPE_CHECKING

def first_and_rest[T, *Ts](first: T, *rest: *Ts) -> tuple[T, tuple[*Ts]]:
    return first, rest

result = first_and_rest(1, "hello", 3.14)
print(result)

if TYPE_CHECKING:
    reveal_type(result)
```

```{code-cell} python
!mypy --strict /tmp/typevartuple_demo.py
!python /tmp/typevartuple_demo.py
```

`T` — тип первого аргумента, а `Ts` — типы оставшихся, здесь `str` и `float`.
Первый аргумент обязателен; оставшихся может быть сколько угодно, в том числе ноль.

---

## 9. Gradual typing

Python — динамический язык. Типы — **опциональный слой**: включаем где помогает, отключаем где мешает.

### `Any` vs `object`

![Any vs object](img/any-vs-object.svg)

`Any` **отключает** проверки. Один `Any` заражает всё вокруг:

```{code-cell} python
%%writefile /tmp/any_leak.py
from typing import Any


def parse(data: Any) -> Any:
    return data["result"]


x = parse({"result": 42})
y = x + "wrong"  # runtime TypeError; mypy думал x: Any -> всё ок
```

```{code-cell} python
!uv run mypy --strict /tmp/any_leak.py
```

<div class="adm-danger" style="background: #8B0000; border: 2px dashed #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Правило:</b> <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Any</code> — только на <b>границе</b> с нетипизированным кодом. Внутри своей логики — никогда.

<b>Плохо:</b> <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">def process(data: Any) -&gt; Any: ...</code>

<b>Хорошо:</b> <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">def process(data: str) -&gt; dict&#91;str, Any&#93;: ...</code> — <code style="background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px;">Any</code> ограничен значениями dict'а, границей внешнего JSON-контракта.

</div>

### Строгий режим по одному модулю

Реальный проект 50k строк без типов. Включить `strict` глобально — нереально: сотни ошибок сразу. Включаем по одному модулю: строгий mypy для новых модулей, `ignore_errors` для старых. Постепенно двигаем модули из "игнора" в "строго".

```toml
[tool.mypy]
python_version = "3.14"
strict = true

[[tool.mypy.overrides]]
module = ["myapp.legacy.*"]
ignore_errors = true

[[tool.mypy.overrides]]
module = ["some_untyped_lib.*"]
ignore_missing_imports = true
```

### `# type: ignore[код]` — с явным кодом

```python
result = untyped_lib.get()  # type: ignore                   — плохо (закроет любую будущую ошибку)
result = untyped_lib.get()  # type: ignore[no-untyped-call]  — хорошо
```

<div class="adm-note" style="background: #44944A; border: 1px solid #fbfbfb; border-radius: 10px; color: #fff; padding: 15px; margin: 10px 0;">

<b>Guido van Rossum, Python Language Summit 2016</b>

<i>"Type hints are optional and will always remain that way."</i>

Про то когда включать: <i>"The larger a codebase is, the more benefit it will get from adding annotations and actually checking them with a tool."</i> — практический совет <i>"sooner than you would like to"</i>.

Источник: <a href="https://lwn.net/Articles/690081/" style="color: #ffd580; text-decoration: underline;">LWN — The state of mypy</a>.

</div>

---

## Материалы

- [PEP 484](https://peps.python.org/pep-0484/) — type hints (базовый)
- [PEP 544](https://peps.python.org/pep-0544/) — Protocols
- [PEP 649](https://peps.python.org/pep-0649/) — ленивые аннотации (3.14)
- [PEP 695](https://peps.python.org/pep-0695/) — новый generic-синтаксис
- [PEP 612 — параметры функции через `ParamSpec`](https://peps.python.org/pep-0612/)
- [PEP 646 — наборы типов через `TypeVarTuple`](https://peps.python.org/pep-0646/)
- [mypy: generics](https://mypy.readthedocs.io/en/stable/generics.html)
- [typing docs](https://docs.python.org/3/library/typing.html)
