---
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']}"
```

<style>
/* Admonition-боксы. Работают в light/dark темах — задан явный фон и текст. */
div.adm-note, div.adm-warn, div.adm-danger { padding: 15px; margin: 10px 0; }
div.adm-note   { background: #44944A; border: 1px solid #fbfbfbff; border-radius: 10px; color: #fff; }
div.adm-warn   { background: #FFBA00; border-left: 5px solid #ffcc00; color: #222; }
div.adm-danger { background: #8B0000; border: 2px dashed #ba0606; border-radius: 5px; color: #fff; }
/* Inline code/pre внутри admonition — стандартный светло-серый фон
   сливается с блоком; ставим полупрозрачный контрастный. */
div.adm-note code, div.adm-note pre,
div.adm-danger code, div.adm-danger pre { background: rgba(0,0,0,0.35); color: #fff; padding: 0 4px; border-radius: 3px; }
div.adm-warn code, div.adm-warn pre { background: rgba(0,0,0,0.15); color: #111; padding: 0 4px; border-radius: 3px; }
/* Ссылки — читаемые цвета под каждый фон. */
div.adm-note a, div.adm-danger a { color: #ffd580; text-decoration: underline; }
div.adm-warn a { color: #003366; text-decoration: underline; }
</style>

# Лекция 1. Packaging и tooling

## 0. О курсе

### Команда

Лектор — **[Иван Лущ](https://t.me/Ch0p1k3)**.

| Группа | 1 | 2 | 3 | 4 | 1 (ЭАД) |
|---|---|---|---|---|---|
| Семинарист | [Даниэль Хайбулин](https://t.me/kiDaniel) | [Михаил Федоров](https://t.me/mfgnik) | [Дарья Оникова](https://t.me/on1kova) | [Андрей Новгородский](https://t.me/codeforcesrankzero) | [Федор Наумов](https://t.me/fsnaumov) |

### Программа

Курс идёт **от языка к системе** — 13 лекций:

- **Блок I. Фундамент + инженерия** — packaging, типы, тесты, отладка (1-4).
- **Блок II. Concurrency** — threading, multiprocessing, async (5-7).
- **Блок III. Метапрограммирование** — дескрипторы, метаклассы (8-9).
- **Блок IV. Внутренности CPython** — JIT, memory/GC, profiling (10-12).
- **Блок V. Наружу** — C-extensions и bindings (13).

### Материалы и связь

Задачи, скорборд и дедлайны — [**manytask**](https://hsemanytask.org/ami-python-advanced)
(поверх [SourceCraft](https://sourcecraft.dev), регистрация через Яндекс ID;
при регистрации создаётся ваш приватный форк — туда пушите).

|   |   |
|---|---|
| ![канал](qr-channel.png) | ![чат](qr-chat.png) |
| [Канал с анонсами](https://t.me/+U3Lc4H5eokNkYWY6) | [Чат курса](https://t.me/+pZqsKY9Ine5jYzg6) |

### Оценка

```
Oитог = 0.2 * семинары + 0.5 * задачи + 0.3 * мок-интервью
```

- **Семинары** — активность на семинарах.
- **Задачи** — регулярные ДЗ через manytask, с автопроверкой в CI. **Защита
  решений.** Преподаватель вправе в любой момент семестра вызвать студента
  на устную защиту любой сданной задачи или группы задач. Студент обязан
  объяснить свое решение, работу кода и используемые языковые конструкции.
  Если студент не может дать связного объяснения по существу, баллы за
  всю соответствующую группу задач в manytask обнуляются; повторная сдача
  этих задач не восстанавливает балл. Если провалено больше одной защиты,
  преподаватель вправе написать дисциплинарную записку за академические
  нарушения студента.
- **Мок-интервью** — устная форма контроля в формате технического Python-
  интервью. Проходит на последних 2-3 лекциях семестра (декабрь).
  Часть I: 4-5 открытых теоретических вопросов равномерно по блокам курса
  ("почему `list[Dog]` нельзя передать вместо `list[Animal]`, а
  `Sequence[Dog]` — можно?", "чем `logger.error(exc)` хуже
  `logger.exception()`?"). Часть II: разбор нескольких задач из сданных —
  как устроено решение, почему выбрал такой подход, какие компромиссы.
  Каждая часть до 5 баллов, итого 0-10.

---

## 1. Что мы собираем: sdist и wheel

Два формата дистрибуции. Оба живут на PyPI, оба знает `pip install`.

**sdist** (source distribution, `.tar.gz`): исходники + `pyproject.toml`.
На целевой машине из sdist собирается wheel локально (нужен компилятор
если есть C-код).

**wheel** (built distribution, `.whl`): готовые файлы, `pip install`
их просто распаковывает в `site-packages/`. Никакой сборки на клиенте.

Имя wheel — `{name}-{version}-{python-tag}-{abi-tag}-{platform-tag}.whl`:

- `greet-0.1.0-py3-none-any.whl` — pure Python, работает где угодно.
- `numpy-2.1.0-cp314-cp314-macosx_15_0_arm64.whl` — только CPython 3.14
  ABI, только macOS 15+ на arm64.

Логика `pip install foo`:

1. Ищет wheel под твою платформу, качает и распаковывает.
2. Если wheel'а нет, качает sdist и собирает wheel локально, потом ставит.
3. `pip install .` делает то же самое, только берёт source из локальной директории.

Значит наш `pyproject.toml` должен описать пакет так, чтобы `pip`/`uv`
знали как собрать оба формата.

---

## 2. `pyproject.toml`

Раньше в корне жил зоопарк: `setup.py`, `setup.cfg`, `MANIFEST.in`,
`requirements.txt`, `requirements-dev.txt`, `.flake8`, `mypy.ini`,
`pytest.ini`, `.coveragerc`, `tox.ini`. Всё это переехало в один
`pyproject.toml` через три PEP:

- **[PEP 518](https://peps.python.org/pep-0518/)** (2016) — `[build-system]`: чем и как собирать.
- **[PEP 517](https://peps.python.org/pep-0517/)** (2015-2017) — интерфейс frontend'а (pip) с backend'ом (setuptools/hatchling/...).
- **[PEP 621](https://peps.python.org/pep-0621/)** (2020) — `[project]`: стандартные метаданные, одинаковые для всех backend'ов.

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Когда стало работать</b>

`pyproject.toml` файл существует с 2016 (PEP 518). Полностью с
`[project]` секцией — только с 2022 (setuptools 61 + pip 22). Раньше
проекты жили с гибридом `pyproject.toml` + `setup.py`.

</div>

Живой пример — `lectures/01-packaging/demo-project/`:

```{code-cell} python
!cat demo-project/pyproject.toml
```

Разберём по секциям.

### `[build-system]` — чем собирать

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

- `requires` — что нужно **на моменте сборки** (не в runtime!). Обычно
  сам backend + иногда `cython`/`cmake` для extensions.
- `build-backend` — модуль который вызывается frontend'ом. Экспортирует
  функции `build_sdist`, `build_wheel`, `build_editable`.

Frontend (`pip`, `uv`, `python -m build`) не знает как собирать твой
код. Он делегирует backend'у через стандартный API PEP 517:

```
pip install .
   -> читает [build-system] из pyproject.toml
   -> ставит `requires` в изолированный build-env
   -> import build-backend, вызывает build_wheel()
   -> получает .whl, ставит в целевой venv
```

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Изолированный build-env</b>

`pip` создаёт временный venv в `/tmp/pip-build-env-*`, ставит туда
`requires`, вызывает backend оттуда. После сборки удаляется. Изоляция —
чтобы твоя старая версия `setuptools` не сломала сборку зависимости
которая требует свежую.

</div>

### Backend'ов много, зачем?

Три причины. Первая: PEP 517/518 стандартизировали интерфейс между
frontend'ом и backend'ом, и появилась возможность писать альтернативы
`setuptools` (до этого он был единственным). Вторая: разным типам
кода нужны разные инструменты. Третья: некоторые backends идут в
связке со своим CLI (`poetry`, `hatch`, `pdm`) и претендуют на весь
workflow.

**Дерево выбора:**

- **Pure Python** — `hatchling` (по умолчанию в 2026). Минималистично — `flit_core`.
- **C/C++ extensions простые** — `setuptools` (legacy, много примеров).
- **C/C++ сложные с CMake** — `scikit-build-core`.
- **Rust через PyO3** — `maturin` (единственный разумный выбор).
- **Уже в экосистеме poetry/pdm** — их родные backends.

Мы используем **hatchling** — pure Python, PEP 621 native, без своих
частных секций.

Подробнее про **manager vs backend** написано ниже; пока просто запомни: `hatchling.build` это backend (мы им пользуемся), а `hatch` это отдельный CLI-инструмент, названия у них
просто похожие.

### Frontend'ов тоже несколько

Frontend — тот кто **вызывает** backend через PEP 517 API. Два подвида:

**Install-frontend** — ставит пакет в venv (frontend → backend вернёт
wheel → frontend распакует в site-packages):

- **`pip install`** — из stdlib, классика, работает всегда.
- **`uv pip install`** / **`uv add`** — тот же интерфейс что у `pip`,
  но заметно быстрее на больших зависимостях.
- **`pipx install`** — для standalone-CLI утилит, каждый в свой изолированный venv.
- **`poetry install`** / **`pdm install`** / **`hatch install`** — в рамках своих workflow.

**Build-frontend** — только собирает `dist/*.tar.gz` + `dist/*.whl`, не
ставит:

- **`python -m build`** — референсная реализация от PyPA, минимальная.
- **`uv build`** — то же, входит в комплект `uv`.
- **`hatch build`** / **`poetry build`** / **`pdm build`** — в рамках их
  экосистем.

Живая проверка что все build-frontends равнозначны (собирают тот же
wheel из одного `pyproject.toml`):

```{code-cell} python
%%bash
# 1. Референсный build-frontend от PyPA - тянем `build` через `uv run --with`
# (temp-install только на время вызова, ничего не оседает в venv лекции).
rm -rf demo-project/dist-a
uv run --with build --no-project -- \
    python -m build --outdir demo-project/dist-a demo-project 2>&1 | tail -3
ls demo-project/dist-a/
```

```{code-cell} python
%%bash
# 2. uv build — то же самое
rm -rf demo-project/dist-b
uv build --out-dir demo-project/dist-b demo-project 2>&1 | tail -2
ls demo-project/dist-b/
```

Проверим — считаем sha256 обоих артефактов:

```{code-cell} python
import hashlib
from pathlib import Path

for name in ("greet-0.1.0-py3-none-any.whl", "greet-0.1.0.tar.gz"):
    a = (Path("demo-project/dist-a") / name).read_bytes()
    b = (Path("demo-project/dist-b") / name).read_bytes()
    print(f"{name}")
    print(f"  sha256(a) = {hashlib.sha256(a).hexdigest()[:24]}...")
    print(f"  sha256(b) = {hashlib.sha256(b).hexdigest()[:24]}...")
    print(f"  identical: {a == b}")
    print()
```

- **`.whl`** — hash совпадает. Wheel — обычный zip, `hatchling` при
  сборке пинит timestamps всех записей на фиксированную дату
  (2020-02-02), поэтому один и тот же исходник даёт побайтово одинаковый
  файл в любом frontend'е. Это стандарт [Reproducible Builds](https://reproducible-builds.org/):
  разные машины/пользователи получают идентичный артефакт → можно
  сверять по hash что никто не подменил wheel в дистрибуции.
- **`.tar.gz`** — hash различается. Внутри tar-архива timestamps
  файлов тоже пришпилены (hatchling делает то же самое). Но gzip-
  обёртка вокруг tar записывает в свой заголовок время сжатия — оно
  каждый раз своё, отсюда разный итоговый файл. Это ограничение gzip
  как формата, а не самого backend'а — распакованное содержимое
  идентично побайтово.

Итог: frontend'ы взаимозаменяемы, они говорят с одним и тем же
`hatchling.build` через PEP 517 API. За воспроизводимость отвечает
backend, а не frontend.

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Полный набор из pipe-line'а</b>

Пакетный workflow разбит на слои. Каждый — независимая ось выбора:

- **Manager**: `pip`+`venv` / `uv` / `poetry` / `hatch` / `pdm` — что
  оркестрирует.
- **Build-frontend**: `python -m build` / `uv build` / `<manager> build` —
  что вызывает backend.
- **Backend**: `hatchling` / `setuptools` / `flit_core` / `maturin` —
  что собирает.
- **Install-frontend**: `pip install` / `uv pip install` — что кладёт
  wheel в venv.

Комбинации свободные. У нас: `hatchling` backend + `uv` manager (и
install, и build).

</div>

### `[project]` — метаданные PEP 621

```{code-cell} python
!sed -n '5,20p' demo-project/pyproject.toml
```

Полный набор полей:

```toml
[project]
name = "greet"                       # PyPI name
version = "0.1.0"                    # SemVer, или dynamic
description = "..."                  # для PyPI карточки
readme = "README.md"
requires-python = ">=3.14"
license = { text = "MIT" }           # или { file = "LICENSE" }
authors = [{ name = "...", email = "..." }]
classifiers = ["Programming Language :: Python :: 3.14"]
dependencies = ["requests>=2.31", "click~=8.1"]

[project.urls]
Homepage = "https://example.com"
Repository = "https://github.com/user/greet"
```

### PEP 508: syntax зависимостей

```
requests>=2.31,<3.0
click~=8.1                            # compatible release == >=8.1,<9.0
numpy>=1.26; python_version >= "3.14" # environment marker
urllib3[secure]>=2.0                  # extras
```

`~=X.Y` = `>=X.Y,<(X+1).0`. Markers: `os_name`, `sys_platform`,
`python_version`, `implementation_name`.

### Extras

```toml
[project.optional-dependencies]
test = ["pytest>=8", "hypothesis>=6.100"]
lint = ["ruff>=0.8", "mypy>=1.13"]
dev = ["greet[test,lint]", "ipython"]   # self-ref
```

Ставится `pip install .[test,lint]`.

### CLI entry points

```toml
[project.scripts]
greet = "greet.cli:main"
```

После установки в venv появляется исполняемый `greet` в `bin/`:

```{code-cell} python
%%bash
rm -rf /tmp/greet-cli
python3 -m venv /tmp/greet-cli
/tmp/greet-cli/bin/pip install --quiet ./demo-project
/tmp/greet-cli/bin/greet Ivan
```

Что внутри `bin/greet`:

```{code-cell} python
!head -5 /tmp/greet-cli/bin/greet
```

### `[tool.*]` — конфиг тулов

Конвенция PEP 518: любой тул читает `[tool.<name>]`. У нас в demo:

```{code-cell} python
!grep -A2 '^\[tool' demo-project/pyproject.toml
```

### Пакет с иерархией: demo-ecommerce

`greet` — это плоский пакет из одного модуля. Реально код чаще
выглядит как иерархия: пакеты с подпакетами, `__init__.py`,
относительные импорты.

В `lectures/01-packaging/demo-ecommerce/` — тот самый **`ecommerce`** из
[базового курса Даниэля](https://github.com/DanielShinoda/ami_python_25_lectures/tree/main/lectures/13.%20%D0%9E%D0%BA%D1%80%D1%83%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.%20%D0%9F%D0%B0%D0%BA%D0%B5%D1%82%D1%8B%20%D0%B8%20%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D0%B8/%D0%9F%D0%B0%D0%BA%D0%B5%D1%82%D1%8B/%D0%9F%D1%80%D0%BE%D0%B5%D0%BA%D1%82%201),
но с добавленным `pyproject.toml`:

```{code-cell} python
%%bash
find demo-ecommerce -type f | sort | grep -v __pycache__ | grep -v dist
```

Тот же код который вы видели у Даниэля — `ecommerce/products.py` с
абсолютными импортами, `ecommerce/payments/stripe.py` с относительными,
`from .paypal import *` в `__init__.py`.

Наша задача: сделать этот код **installable**. Всё что нужно — один файл `pyproject.toml`:

```{code-cell} python
!cat demo-ecommerce/pyproject.toml
```

Ключевое отличие от `demo-project/greet`:

```toml
[tool.hatch.build.targets.wheel]
packages = ["src/ecommerce"]     # где лежит пакет — src-layout
```

Ставим и запускаем:

```{code-cell} python
%%bash
rm -rf /tmp/venv-ecom
python3 -m venv /tmp/venv-ecom
/tmp/venv-ecom/bin/pip install --quiet ./demo-ecommerce
/tmp/venv-ecom/bin/python demo-ecommerce/main.py
```

Обрати внимание на порядок вывода — видно порядок загрузки модулей:
`ecommerce` → его `__init__.py` → импортит `ecommerce.products` →
тот тянет `ecommerce.payments` → тянет `paypal` и `stripe` (через `*`)
→ каждый тянет `utils` (свой + parent's). **`import` = выполнение кода.**

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Почему `__init__.py` не должен делать много</b>

Каждый `import` тянет весь этот граф. Если в `__init__.py` тяжёлая
инициализация (открытие БД, чтение файлов, network) — она произойдёт
при первом импорте, часто в неожиданный момент. Держи `__init__.py`
чистым (re-exports + `__all__`).

</div>

Смотрим что попало в wheel:

```{code-cell} python
%%bash
cd demo-ecommerce
rm -rf dist/
uv build 2>&1 | tail -3
```

```{code-cell} python
import zipfile
with zipfile.ZipFile("demo-ecommerce/dist/ecommerce-0.1.0-py3-none-any.whl") as z:
    for name in z.namelist():
        print(name)
```

Все `.py` файлы из `src/ecommerce/` попали в wheel как `ecommerce/`.
Иерархия сохранена, `main.py` внутрь **не** попал (это скрипт-запускатор,
не часть пакета).

---

## 3. Сборка через backend — потрошим wheel

```{code-cell} python
%%bash
cd demo-project
rm -rf dist/
uv build 2>&1 | tail -5
ls -la dist/
```

Wheel — это ZIP. Смотрим через `zipfile` из stdlib (никакого unzip):

```{code-cell} python
import zipfile, glob

wheel_path = glob.glob("demo-project/dist/*.whl")[0]
with zipfile.ZipFile(wheel_path) as z:
    for name in z.namelist():
        print(name)
```

Что за `dist-info/*` файлы:

- **METADATA** — плоский формат `[project]` (RFC 5322, headers-like).
- **WHEEL** — версия формата + platform tags.
- **RECORD** — CSV `path,hash,size` для integrity при uninstall.
- **entry_points.txt** — если есть `[project.scripts]`.

METADATA изнутри:

```{code-cell} python
with zipfile.ZipFile(wheel_path) as z:
    metadata_name = next(n for n in z.namelist() if n.endswith("METADATA"))
    print(z.read(metadata_name).decode())
```

Ту же самую информацию pip читает при `pip install` — без запуска
никакого Python-кода.

Что в sdist:

```{code-cell} python
import tarfile

sdist_path = glob.glob("demo-project/dist/*.tar.gz")[0]
with tarfile.open(sdist_path) as t:
    for member in t.getnames():
        print(member)
```

Внутри sdist — исходники (в отличие от wheel где только собранные `.py`
плюс metadata).

### PEP 517 build protocol вживую

Что делает `uv build` под капотом:

1. Читает `[build-system]` — видит `hatchling.build`.
2. Ставит `hatchling` в изолированный build-env.
3. `import hatchling.build; hatchling.build.build_wheel(...)`.
4. Backend собирает файлы, кладёт wheel в `dist/`.

Смотрим шаги через `pip install -v`:

```{code-cell} python
%%bash
rm -rf /tmp/build-verbose
python3 -m venv /tmp/build-verbose
/tmp/build-verbose/bin/pip install ./demo-project -v 2>&1 \
    | grep -E "^(Created temporary|Running|Building|Installing|Successfully)" \
    | head -20
```

---

## 4. PyPI — куда всё это едет

**PyPI** ([pypi.org](https://pypi.org)) — центральный индекс Python-пакетов.
~600k пакетов в 2026, ~1M загрузок в секунду. Основан 2003, ранее назывался
"cheese shop" (Monty Python отсылка).

Когда пишешь `pip install requests` — pip идёт **именно** сюда. Как
именно? Через **Simple API** ([PEP 503](https://peps.python.org/pep-0503/)):

```{code-cell} python
%%bash
# Simple API — просто HTML-страница со списком wheels пакета
curl -s https://pypi.org/simple/requests/ | head -20
```

Список ссылок на wheels и sdist. Pip качает эту страницу, находит
самую свежую версию, выбирает wheel под твою платформу, качает.

### JSON API — детально про пакет

```{code-cell} python
%%bash
curl -s https://pypi.org/pypi/requests/json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('name:', d['info']['name'])
print('version:', d['info']['version'])
print('requires_python:', d['info']['requires_python'])
print('license:', d['info']['license'])
print('summary:', d['info']['summary'])
print('project_urls:', list(d['info']['project_urls'].keys())[:5])
print('files (latest):', len(d['urls']), 'вариантов wheel/sdist')
"
```

### Как pip выбирает wheel

Смотрим что pip **скачал бы** для нас, без установки:

```{code-cell} python
%%bash
rm -rf /tmp/pypi-demo
mkdir /tmp/pypi-demo
pip download requests --no-deps -d /tmp/pypi-demo 2>&1 | tail -3
ls /tmp/pypi-demo/
```

Выбрал `requests-*.whl` под нашу платформу (`py3-none-any` — pure
Python, работает везде).

### Публикация — обратный путь

Мы пока ничего не публикуем, но flow тот же для всех:

```bash
uv build                  # dist/*.tar.gz + dist/*.whl
uv publish                # → грузит в PyPI (API token из pypi.org/manage/account/token/)
# или классически:
pip install twine
twine upload dist/*
```

**Test PyPI** ([test.pypi.org](https://test.pypi.org)) — sandbox для
проверки перед прод:

```bash
uv publish --publish-url https://test.pypi.org/legacy/
pip install --index-url https://test.pypi.org/simple/ mypack
```

### Приватные индексы

Компании часто держат свой PyPI-совместимый индекс (Artifactory, Nexus,
`devpi`, `pypiserver`, GitHub Packages). Все совместимы с PEP 503, значит
работают с `pip`/`uv` из коробки:

```bash
pip install --index-url https://pypi.internal.company.com/simple/ mypack
# Или два индекса — сначала свой, потом public fallback:
pip install --extra-index-url https://pypi.internal.company.com/simple/ mypack
```

Аналогично для PyTorch с CUDA-wheels — они на своём индексе,
`pip install torch --extra-index-url https://download.pytorch.org/whl/cu124`.

### Не только PyPI: git, tarball, local path

`pip` умеет ставить не только с PyPI. Полезно когда: пакет не опубликован,
нужна dev-ветка, форк с фиксом, monorepo с несколькими пакетами внутри.

**Прямо из git:**

```bash
# Последний коммит main:
pip install git+https://github.com/user/repo.git

# Конкретная ветка / тег / SHA:
pip install git+https://github.com/user/repo.git@develop
pip install git+https://github.com/user/repo.git@v1.2.3
pip install git+https://github.com/user/repo.git@a1b2c3d

# Через SSH (если есть ключ):
pip install git+ssh://git@github.com/user/repo.git

# Пакет из subdirectory monorepo:
pip install "mypkg @ git+https://github.com/org/repo.git@main#subdirectory=mypkg"
```

Синтаксис `имя @ URL` — [PEP 508 direct URL](https://peps.python.org/pep-0508/#direct-references).
Работает и в `pyproject.toml.dependencies`, и на CLI, и в `requirements.txt`.

**Через tarball / архив URL:**

```bash
# GitHub генерирует .tar.gz для любого ref:
pip install https://github.com/user/repo/archive/refs/heads/main.tar.gz
pip install https://github.com/user/repo/archive/refs/tags/v1.0.tar.gz
```

Быстрее чем `git+https` (не тянет всю историю).

**Local path:**

```bash
pip install ./demo-project              # обычная установка
pip install -e ./demo-project           # editable
pip install ./demo-project[test,lint]   # с extras
```

Именно это делает `pip install .` — берёт `.` как local path.

<style>div.adm-danger{background:#8B0000;color:#fff;padding:15px;margin:10px 0;border:2px dashed #ba0606;border-radius:5px}div.adm-danger code,div.adm-danger pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-danger a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-danger">

<b>Воспроизводимость сборки: фиксируй commit hash</b>

`pip install git+https://.../repo.git@main` — **на каждой сборке разный
код**. `main` двигается, и через месяц соберётся другой пакет.

Правильно — закрепить конкретный SHA:

    mypkg @ git+https://github.com/org/repo.git@a1b2c3d#subdirectory=mypkg

Или ещё лучше — сначала tag/release с версией, потом использовать `==`
через lockfile. Git-URL с тегом — компромисс (тег теоретически можно
перевесить, но обычно не двигают).

</div>

### Security

<style>div.adm-danger{background:#8B0000;color:#fff;padding:15px;margin:10px 0;border:2px dashed #ba0606;border-radius:5px}div.adm-danger code,div.adm-danger pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-danger a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-danger">

<b>Typosquatting</b>

Пакет `requests` — настоящий, официальный. А вот `request` (без -s),
`requeests`, `python-requests` — вредоносные копии с похожим именем. Всегда сверяй имя перед `pip install`.

</div>

Реальные инциденты: `colourama` (2017, вместо `colorama`), `python-dateutil` копии,
десятки в 2020-2024. `pip install` = **выполнение произвольного кода** от
автора пакета — доверяй только известным.

Как защититься:
- `pip install --require-hashes` + `requirements.txt` с `--hash=sha256:...` — pip
  сверит SHA256 wheel'а с зафиксированным. Тот же принцип у `uv.lock`.
- Подписанные пакеты ([PEP 458](https://peps.python.org/pep-0458/) — в
  процессе внедрения) — TUF-based подписи, работа PSF/Warehouse.
- **`pip install .` из непроверенного sdist = запуск чужого `setup.py`.** Это
  одна из причин ухода от executable `setup.py` к декларативному
  `pyproject.toml`.

### Полезные команды

```{code-cell} python
!pip show requests 2>&1 | head -8
```

`pip show <name>` — локально установленная версия + metadata + место
(`Location: ...`).

```bash
pip index versions requests   # все доступные версии
pip show --files requests     # какие файлы установились
pip check                     # broken dependencies
```

---

## 5. Установка: `pip` vs `uv`

`pip` — это install-frontend из stdlib, работает всегда. `uv` даёт
тот же интерфейс и заметно быстрее на больших зависимостях. Учиться
командам заново не надо, `uv pip install X` делает ровно то же, что
`pip install X`.

### Сначала — виртуальное окружение

**Никогда** не ставь пакеты в системный Python (`/usr/bin/python` или
Homebrew). Причины:

- Права: `pip install` в системный Python требует `sudo` — легко
  накатить что-то, что сломает утилиты системы, зависящие от Python.
- Изоляция версий: проект A хочет `requests==2.28`, B хочет
  `requests==2.31`. В одном системном Python держать оба нельзя.
- Reproducibility: `pip freeze` системного Python — свалка всего когда-
  либо ставленного, воспроизвести на другой машине невозможно.

Решение — **виртуальное окружение** (venv, PEP 405). Это отдельный
каталог с копией Python и своим `site-packages/`. Чтобы им
пользоваться, надо его активировать — подменить `$PATH` так, чтобы
`python` и `pip` шли из этого каталога:

```{code-cell} python
%%bash
python3 -m venv /tmp/example-venv
source /tmp/example-venv/bin/activate
which python pip
python -c "import sys; print(sys.prefix)"
```

Внутри активированного venv `pip install X` кладёт X в
`/tmp/example-venv/lib/pythonX.Y/site-packages/`, а не в системный. Всё
что ставится в venv — живёт только там, `rm -rf /tmp/example-venv/` и
всё удалено.

`uv` идёт дальше и делает venv за тебя автоматически: `uv sync` в
проекте создаёт `.venv/`, `uv run` — запускает команду в нём без явной
активации.

### pip + venv (классика)

```{code-cell} python
%%bash
rm -rf /tmp/venv-pip
python3 -m venv /tmp/venv-pip
/tmp/venv-pip/bin/pip install --quiet -e './demo-project[test,lint]'
/tmp/venv-pip/bin/pip list | head -10
```

### uv (современно)

```{code-cell} python
%%bash
rm -rf /tmp/venv-uv
uv venv /tmp/venv-uv
uv pip install --python /tmp/venv-uv/bin/python -e './demo-project[test,lint]' --quiet
uv pip list --python /tmp/venv-uv/bin/python | head -10
```

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>В курсе — оба</b>

Проверяющий скрипт использует `pip install .[test]`, интерфейс общий —
подходит и pip-venv, и uv-venv. Выбирайте что удобнее; `uv` быстрее,
`pip` без внешних зависимостей.

</div>

### Editable install: зачем и почему

Показываем на живом примере что не так с обычным `pip install .` для
активной разработки.

**Шаг 1.** Копируем demo-project во временную директорию (чтобы
экспериментировать не портя оригинал), ставим обычным способом,
проверяем что работает:

```{code-cell} python
%%bash
rm -rf /tmp/greet-exp /tmp/venv-no-e
cp -r demo-project /tmp/greet-exp

python3 -m venv /tmp/venv-no-e
/tmp/venv-no-e/bin/pip install --quiet /tmp/greet-exp
/tmp/venv-no-e/bin/python -c "from greet import greet; print(greet('world'))"
```

**Шаг 2.** Правим исходник — меняем `Hello,` на `HI,`:

```{code-cell} python
!perl -i -pe 's/Hello,/HI,/' /tmp/greet-exp/src/greet/__init__.py
!cat /tmp/greet-exp/src/greet/__init__.py
```

Source изменился. Но:

```{code-cell} python
!/tmp/venv-no-e/bin/python -c "from greet import greet; print(greet('world'))"
```

**По-прежнему `Hello, world!`** — потому что `pip install .` скопировал
код в `site-packages/` при установке, там лежит **старая версия**. Наш
edit в source-каталоге к установленному пакету не относится.

**Шаг 3.** Что можно сделать — переустановить:

```{code-cell} python
%%bash
/tmp/venv-no-e/bin/pip install --quiet --force-reinstall --no-deps /tmp/greet-exp
/tmp/venv-no-e/bin/python -c "from greet import greet; print(greet('world'))"
```

Теперь `HI, world!`. Но представь что цикл «правка → тест» происходит
каждые 30 секунд — каждый раз переустанавливать невыносимо.

### Лечение — `-e`

`pip install -e /path` — **editable install**. Пакет как бы "ставится",
но `site-packages/` содержит не копию, а **ссылку** на source-каталог.
`import` идёт напрямую в твои файлы.

```{code-cell} python
%%bash
rm -rf /tmp/venv-e
python3 -m venv /tmp/venv-e
/tmp/venv-e/bin/pip install --quiet -e /tmp/greet-exp
/tmp/venv-e/bin/python -c "from greet import greet; print(greet('world'))"
```

Правим source ещё раз — теперь `HI,` -> `HEY,`:

```{code-cell} python
!perl -i -pe 's/HI,/HEY,/' /tmp/greet-exp/src/greet/__init__.py
!/tmp/venv-e/bin/python -c "from greet import greet; print(greet('world'))"
```

**Сразу видит `HEY, world!`** — никакой переустановки. Именно то что
нужно для активной разработки.

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Когда использовать</b>

- **Разработка своего пакета** — всегда `-e`, чтобы правки не требовали
  переустановки.
- **Дебаг чужого форка** — `-e ~/forks/somelib` + правки прямо в клоне.
- **Не для прода** — deploy всегда обычный `pip install .` (без `-e`).

</div>

### Под капотом: `.pth` файл (PEP 660)

Механизм — файл `.pth` в `site-packages/`. Python при старте читает
все `*.pth` в этом каталоге и добавляет каждую строку как путь в
`sys.path`. У нас в venv-e:

```{code-cell} python
!find /tmp/venv-e/lib -name "__editable__*.pth" -exec cat {} \;
```

Одна строка — абсолютный путь к нашему `/tmp/greet-exp/src`. При
`import greet` Python идёт по `sys.path`, находит
`/tmp/greet-exp/src/greet/__init__.py` и импортит **оттуда** — прямо
из наших исходников. Никаких копий.

PEP 660 (2021) стандартизировал API editable install между backend'ами —
до этого каждый (setuptools, poetry, flit) реализовывал по-своему.

### Lock files

`pyproject.toml.dependencies` — **диапазоны** (`requests>=2.31`). Для
deploy нужны **точные версии** — иначе прод и dev расходятся.

Два подхода:

- **`pip freeze > requirements.txt`** — плоский `name==version`. Без
  hashes, без extras, без markers.
- **`uv.lock` / `poetry.lock` / `pdm.lock`** — универсальный формат,
  кросс-платформенный, с hashes, разделение direct/transitive.

```{code-cell} python
%%bash
cd demo-project
rm -f uv.lock
uv lock --quiet 2>&1 || true
head -25 uv.lock 2>/dev/null | head -20
```

Lock коммитим в git. `uv sync` на любой машине даст тот же venv.

### Полные менеджеры: poetry, hatch, pdm, uv

`pip`, `uv pip`, `venv`, `hatchling` — это **отдельные слои**. Некоторые
tools объединяют всё в один CLI: сборка + deps + venv + publish + lock +
version bump + Python-версия.

**Poetry** (2018, Sébastien Eustace) — самый популярный full-workflow.
Свой lock, до 2024 свой `[tool.poetry]` формат вместо PEP 621.

```{code-cell} python
!uv tool run --with poetry poetry --version
```

**Hatch** (2022, Ofek Lev / PSF core) — full workflow, полностью
PEP 621. Сильная сторона — **матрица окружений**: в `[tool.hatch.envs]`
описываешь несколько сред (`default`, `test`, `docs`, ...), в каждой
свой набор зависимостей и Python-версия, дальше `hatch run test:pytest`
прогоняет тесты в каждой из них. Аналог `tox` из старых времён
(старый инструмент того же класса, до сих пор используется).

```{code-cell} python
!uv tool run --with hatch hatch --version
```

**PDM** (2020, Frost Ming) — начинался с эксперимента PEP 582
(`__pypackages__` вместо venv, идея как `node_modules`). Сейчас — обычный
CLI + lock, PEP 621 native.

```{code-cell} python
!uv tool run --with pdm pdm --version
```

**uv** (2024, Astral) — новый инструмент, вобрал лучшее из остальных.
Совместим с `pip`: команды `uv pip install`, `uv pip list` работают
ровно как `pip`, без правок скриптов. Плюс полный workflow: `uv init`,
`uv add`, `uv sync`, `uv run`, `uv publish`, `uv tool install` (аналог
pipx), `uv python install` (аналог pyenv).

**Одна и та же операция в разных инструментах:**

| Действие | pip + venv | poetry | hatch | pdm | uv |
|----------|-----------|--------|-------|-----|-----|
| Init | `python -m venv .venv` | `poetry new` | `hatch new` | `pdm init` | `uv init` |
| Add dep | `pip install X` + правка `[project]` | `poetry add X` | `hatch add X` | `pdm add X` | `uv add X` |
| Install | `pip install .` | `poetry install` | `hatch install` | `pdm install` | `uv sync` |
| Lock | `pip freeze` (не PEP) | `poetry lock` | нет своего | `pdm lock` | `uv lock` |
| Run | `.venv/bin/python` | `poetry run` | `hatch run` | `pdm run` | `uv run` |
| Publish | `twine upload` | `poetry publish` | `hatch publish` | `pdm publish` | `uv publish` |

<style>div.adm-warn{background:#FFBA00;color:#222;padding:15px;margin:10px 0;border-left:5px solid #ffcc00}div.adm-warn code,div.adm-warn pre{background:rgba(0,0,0,0.15);color:#111;padding:0 4px;border-radius:3px}div.adm-warn a{color:#003366;text-decoration:underline}</style>
<div class="adm-warn">

<b>Poetry lock несовместим</b>

`poetry.lock` — только poetry читает. `hatch`/`pdm`/`uv`/`pip` не понимают.
То же обратно — poetry не читает чужие lock. Значит перейти с poetry на
что-то другое = **потерять lock**, перегенерировать заново.

</div>

<style>div.adm-warn{background:#FFBA00;color:#222;padding:15px;margin:10px 0;border-left:5px solid #ffcc00}div.adm-warn code,div.adm-warn pre{background:rgba(0,0,0,0.15);color:#111;padding:0 4px;border-radius:3px}div.adm-warn a{color:#003366;text-decoration:underline}</style>
<div class="adm-warn">

<b>Poetry ещё встречает <code>[tool.poetry]</code></b>

Legacy poetry-проекты используют свой `[tool.poetry]` вместо стандартного
`[project]` PEP 621. Внешне похоже, но синтаксис зависимостей отличается
(`^1.0` вместо `>=1.0,<2.0` и т.п.). Poetry 2+ поддерживает `[project]`,
но старые проекты часто ещё на `[tool.poetry]`.

</div>

**Советы:**

- **Новый проект** — `uv`. Один инструмент, быстрый, совместим с `pip`.
- **Наследованный poetry** — оставайся. Миграция редко того стоит.
- **`hatch`** — хорошо для сложных проектов с несколькими environments
  (dev, docs, integration test с разными Python-версиями).
- **`pdm`** — нишевый. Единственная причина брать — если уже
  используешь.

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>Backend и manager — независимые оси</b>

`hatchling.build` (backend, собирает wheel) и `hatch` (manager,
оркестрирует venv/deps/publish) — разные вещи, названия просто
похожие. У нас в `pyproject.toml` `hatchling.build` как backend, а
`uv` как manager — работает без проблем.

То же и с poetry: `poetry.core.masonry.api` — это только backend, его
можно использовать без CLI `poetry`. Или взять backend `flit_core`
плюс manager `uv`. Backend и manager меняются независимо, важно только
чтобы оба понимали `pyproject.toml`.

</div>

---

## 6. Static tooling

Два инструмента, оба с первого дня курса: **`ruff`** и **`mypy --strict`**.

### `ruff` — линтер + форматтер

Заменяет `black` + `isort` + `flake8` + большинство плагинов, работает
быстро на любом размере кодовой базы.

Плохой код:

```{code-cell} python
%%writefile /tmp/bad.py
import os,sys
from typing import  List, Dict

def greet( name:str )->str :
    result="Hello, "+name
    return result

x=greet("world")
print(x)
```

Линтер:

```{code-cell} python
!uv run --no-project --with ruff ruff check /tmp/bad.py || true
```

Autofix:

```{code-cell} python
!uv run --no-project --with ruff ruff check --fix /tmp/bad.py 2>&1 | tail -3
```

Форматтер (аналог `black`):

```{code-cell} python
!uv run --no-project --with ruff ruff format /tmp/bad.py && cat /tmp/bad.py
```

Конфиг:

```toml
[tool.ruff]
line-length = 100
target-version = "py314"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
ignore = ["E501"]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]  # assert allowed в тестах
```

<style>div.adm-warn{background:#FFBA00;color:#222;padding:15px;margin:10px 0;border-left:5px solid #ffcc00}div.adm-warn code,div.adm-warn pre{background:rgba(0,0,0,0.15);color:#111;padding:0 4px;border-radius:3px}div.adm-warn a{color:#003366;text-decoration:underline}</style>
<div class="adm-warn">

<b>Не ставь `select = ["ALL"]`</b>

Там есть правила, которые противоречат друг другу — один включённый
запрещает то, что требует другой, и линтер начинает ругаться на
любой код. Начинай с базового набора, расширяй по мере надобности.

</div>

### `mypy` strict

Без strict — mypy молчит на многом:

```{code-cell} python
%%writefile /tmp/untyped.py
def greet(name):
    return "Hi " + name

greet(42)  # int вместо str - но mypy без strict пропустит
```

Тихо (`--config-file=/dev/null` — чтобы не подхватил конфиг курса
`course-content/pyproject.toml`, где strict уже включён):

```{code-cell} python
!uv run --no-project --with mypy mypy --config-file=/dev/null /tmp/untyped.py 2>&1 | grep -v "No \[mypy\]" || true
```

Со strict — ловит:

```{code-cell} python
!uv run --no-project --with mypy mypy --config-file=/dev/null --strict /tmp/untyped.py 2>&1 | grep -v "No \[mypy\]" || true
```

Fix — типы:

```{code-cell} python
%%writefile /tmp/typed.py
def greet(name: str) -> str:
    return "Hi " + name

greet("world")
```

```{code-cell} python
!uv run --no-project --with mypy mypy --config-file=/dev/null --strict /tmp/typed.py 2>&1 | grep -v "No \[mypy\]"
```

Конфиг:

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

[[tool.mypy.overrides]]
module = ["legacy.*"]           # gradual: пока не готовы
ignore_errors = true

[[tool.mypy.overrides]]
module = ["some_untyped_lib.*"] # чужие либы без stubs
ignore_missing_imports = true
```

Глубоко про типы — Лекция 2.

### `pre-commit` — hooks локально (опционально)

Хочется чтобы ruff/mypy проверяли код **до** push'а, не в CI? Стандартный
инструмент — [pre-commit](https://pre-commit.com/). Ставит git-hook,
который перед `git commit` прогоняет любые проверки. Пример
`.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.13.0
    hooks:
      - id: mypy
        additional_dependencies: [pytest]
```

Установка:

```bash
uv pip install pre-commit
pre-commit install
```

<style>div.adm-note{background:#44944A;color:#fff;padding:15px;margin:10px 0;border:1px solid #fbfbfb;border-radius:10px}div.adm-note code,div.adm-note pre{background:rgba(0,0,0,0.35);color:#fff;padding:0 4px;border-radius:3px}div.adm-note a{color:#ffd580;text-decoration:underline}</style>
<div class="adm-note">

<b>В курсе не требуется</b>

Мы **не** ставим `pre-commit` как часть шаблона задач: студенту это
не обязательно. Кто хочет, настраивает у себя в форке; коммитить
`.pre-commit-config.yaml` в публичный репо не нужно, это твой личный
workflow.

</div>

---

## Материалы

- [Python Packaging User Guide](https://packaging.python.org/en/latest/)
- [PEP 517](https://peps.python.org/pep-0517/) — build backend interface
- [PEP 518](https://peps.python.org/pep-0518/) — `[build-system]`
- [PEP 621](https://peps.python.org/pep-0621/) — `[project]`
- [PEP 508](https://peps.python.org/pep-0508/) — dependency spec
- [PEP 660](https://peps.python.org/pep-0660/) — editable installs
- [uv docs](https://docs.astral.sh/uv/) · [ruff rules](https://docs.astral.sh/ruff/rules/) · [mypy docs](https://mypy.readthedocs.io/)
