Лекция 1. Packaging и tooling#
0. О курсе#
Команда#
Лектор — Иван Лущ.
Группа |
1 |
2 |
3 |
4 |
1 (ЭАД) |
|---|---|---|---|---|---|
Семинарист |
Программа#
Курс идёт от языка к системе — 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 (поверх SourceCraft, регистрация через Яндекс ID; при регистрации создаётся ваш приватный форк — туда пушите).
Оценка#
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:
Ищет wheel под твою платформу, качает и распаковывает.
Если wheel’а нет, качает sdist и собирает wheel локально, потом ставит.
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 (2016) —
[build-system]: чем и как собирать.PEP 517 (2015-2017) — интерфейс frontend’а (pip) с backend’ом (setuptools/hatchling/…).
PEP 621 (2020) —
[project]: стандартные метаданные, одинаковые для всех backend’ов.
Когда стало работать
pyproject.toml файл существует с 2016 (PEP 518). Полностью с
[project] секцией — только с 2022 (setuptools 61 + pip 22). Раньше
проекты жили с гибридом pyproject.toml + setup.py.
Живой пример — lectures/01-packaging/demo-project/:
!cat demo-project/pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "greet"
version = "0.1.0"
description = "Тривиальный пакет для демонстрации packaging"
readme = "README.md"
requires-python = ">=3.14"
authors = [{ name = "Иван Лущ", email = "ch0p1k3@yandex.ru" }]
license = { text = "MIT" }
dependencies = []
[project.optional-dependencies]
test = ["pytest>=8"]
lint = ["ruff>=0.8", "mypy>=1.13"]
[project.scripts]
greet = "greet.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/greet"]
[tool.hatch.build.targets.sdist]
exclude = [".venv*", "dist", "__pycache__", ".pytest_cache", ".ruff_cache", ".mypy_cache"]
[tool.ruff]
line-length = 100
target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
[tool.mypy]
python_version = "3.14"
strict = true
Разберём по секциям.
[build-system] — чем собирать#
[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
Изолированный build-env
pip создаёт временный venv в /tmp/pip-build-env-*, ставит туда
requires, вызывает backend оттуда. После сборки удаляется. Изоляция —
чтобы твоя старая версия setuptools не сломала сборку зависимости
которая требует свежую.
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):
%%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/
- hatchling==1.32.0
* Building wheel...
Successfully built greet-0.1.0.tar.gz
and greet-0.1.0-py3-none-any.whl
greet-0.1.0-py3-none-any.whl
greet-0.1.0.tar.gz
%%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/
Successfully built demo-project/dist-b/greet-0.1.0.tar.gz
Successfully built
demo-project/dist-b/greet-0.1.0-py3-none-any.whl
greet-0.1.0-py3-none-any.whl
greet-0.1.0.tar.gz
Проверим — считаем sha256 обоих артефактов:
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()
greet-0.1.0-py3-none-any.whl
sha256(a) = 01e6c56739512cb5bcaecf4c...
sha256(b) = 01e6c56739512cb5bcaecf4c...
identical: True
greet-0.1.0.tar.gz
sha256(a) = a50d4de4124f5d1a7e3099fd...
sha256(b) = 7e19f34bdefd35b520917fc8...
identical: False
.whl— hash совпадает. Wheel — обычный zip,hatchlingпри сборке пинит timestamps всех записей на фиксированную дату (2020-02-02), поэтому один и тот же исходник даёт побайтово одинаковый файл в любом frontend’е. Это стандарт Reproducible Builds: разные машины/пользователи получают идентичный артефакт → можно сверять по hash что никто не подменил wheel в дистрибуции..tar.gz— hash различается. Внутри tar-архива timestamps файлов тоже пришпилены (hatchling делает то же самое). Но gzip- обёртка вокруг tar записывает в свой заголовок время сжатия — оно каждый раз своё, отсюда разный итоговый файл. Это ограничение gzip как формата, а не самого backend’а — распакованное содержимое идентично побайтово.
Итог: frontend’ы взаимозаменяемы, они говорят с одним и тем же
hatchling.build через PEP 517 API. За воспроизводимость отвечает
backend, а не frontend.
Полный набор из pipe-line’а
Пакетный 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).
[project] — метаданные PEP 621#
!sed -n '5,20p' demo-project/pyproject.toml
[project]
name = "greet"
version = "0.1.0"
description = "Тривиальный пакет для демонстрации packaging"
readme = "README.md"
requires-python = ">=3.14"
authors = [{ name = "Иван Лущ", email = "ch0p1k3@yandex.ru" }]
license = { text = "MIT" }
dependencies = []
[project.optional-dependencies]
test = ["pytest>=8"]
lint = ["ruff>=0.8", "mypy>=1.13"]
[project.scripts]
greet = "greet.cli:main"
Полный набор полей:
[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#
[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#
[project.scripts]
greet = "greet.cli:main"
После установки в venv появляется исполняемый greet в bin/:
%%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
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/greet-cli/bin/python3.14 -m pip install --upgrade pip
Hello, Ivan!
Что внутри bin/greet:
!head -5 /tmp/greet-cli/bin/greet
#!/private/tmp/greet-cli/bin/python3.14
import sys
from greet.cli import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
[tool.*] — конфиг тулов#
Конвенция PEP 518: любой тул читает [tool.<name>]. У нас в demo:
!grep -A2 '^\[tool' demo-project/pyproject.toml
[tool.hatch.build.targets.wheel]
packages = ["src/greet"]
[tool.hatch.build.targets.sdist]
exclude = [".venv*", "dist", "__pycache__", ".pytest_cache", ".ruff_cache", ".mypy_cache"]
[tool.ruff]
line-length = 100
target-version = "py314"
--
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
[tool.mypy]
python_version = "3.14"
strict = true
Пакет с иерархией: demo-ecommerce#
greet — это плоский пакет из одного модуля. Реально код чаще
выглядит как иерархия: пакеты с подпакетами, __init__.py,
относительные импорты.
В lectures/01-packaging/demo-ecommerce/ — тот самый ecommerce из
базового курса Даниэля,
но с добавленным pyproject.toml:
%%bash
find demo-ecommerce -type f | sort | grep -v __pycache__ | grep -v dist
demo-ecommerce/main.py
demo-ecommerce/pyproject.toml
demo-ecommerce/README.md
demo-ecommerce/src/eco
mmerce/__init__.py
demo-ecommerce/src/ecommerce/payments/__init__.py
demo-ecommerce/src/ecommerce/pa
yments/paypal.py
demo-ecommerce/src/ecommerce/payments/stripe.py
demo-ecommerce/src/ecommerce/paymen
ts/utils.py
demo-ecommerce/src/ecommerce/products.py
demo-ecommerce/src/ecommerce/utils.py
Тот же код который вы видели у Даниэля — ecommerce/products.py с
абсолютными импортами, ecommerce/payments/stripe.py с относительными,
from .paypal import * в __init__.py.
Наша задача: сделать этот код installable. Всё что нужно — один файл pyproject.toml:
!cat demo-ecommerce/pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ecommerce"
version = "0.1.0"
description = "Демо иерархического пакета из базового курса Даниэля, оформленный как installable"
readme = "README.md"
requires-python = ">=3.14"
authors = [{ name = "Daniel Shinoda" }, { name = "Ivan Lushch" }]
license = { text = "MIT" }
dependencies = []
[tool.hatch.build.targets.wheel]
packages = ["src/ecommerce"]
[tool.hatch.build.targets.sdist]
exclude = [".venv*", "dist", "__pycache__"]
Ключевое отличие от demo-project/greet:
[tool.hatch.build.targets.wheel]
packages = ["src/ecommerce"] # где лежит пакет — src-layout
Ставим и запускаем:
%%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
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/venv-ecom/bin/python3.14 -m pip install --upgrade pip
Загружается модуль `ecommerce`. Родительский пакет `ecommerce`
З
агружается модуль `ecommerce.products`. Родительский пакет `ecommerc
e`
Загружается модуль `ecommerce.payments`. Родительский пакет `ec
ommerce.payments`
Загружается модуль `ecommerce.payments.paypal`. Родитель
ский пакет `ecommerce.payments`
Загружается модуль `ecommerce.payments.stri
pe`. Родительский пакет `ecommerce.payments`
Загрузился модуль `eco
mmerce.payments`. Родительский пакет `ecommerce.payments`
paypal_func called
paymen
ts utils_func called
ecommerce utils_func called
paypal
stripe_func called
payments utils_func calle
d
ecommerce utils_func called
stripe
Загрузился модуль `ecommerce`. Родитель
ский пакет `ecommerce`
Обрати внимание на порядок вывода — видно порядок загрузки модулей:
ecommerce → его __init__.py → импортит ecommerce.products →
тот тянет ecommerce.payments → тянет paypal и stripe (через *)
→ каждый тянет utils (свой + parent’s). import = выполнение кода.
Почему __init__.py не должен делать много
Каждый import тянет весь этот граф. Если в __init__.py тяжёлая
инициализация (открытие БД, чтение файлов, network) — она произойдёт
при первом импорте, часто в неожиданный момент. Держи __init__.py
чистым (re-exports + __all__).
Смотрим что попало в wheel:
%%bash
cd demo-ecommerce
rm -rf dist/
uv build 2>&1 | tail -3
Building wheel from source distribution...
Successfully built dist/ecommerce-0.1.0.
tar.gz
Successfully built dist/ecommerce-0.1.0-py3-none-any.whl
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)
ecommerce/__init__.py
ecommerce/products.py
ecommerce/utils.py
ecommerce/payments/__init__.py
ecommerce/payments/paypal.py
ecommerce/payments/stripe.py
ecommerce/payments/utils.py
ecommerce-0.1.0.dist-info/METADATA
ecommerce-0.1.0.dist-info/WHEEL
ecommerce-0.1.0.dist-info/RECORD
Все .py файлы из src/ecommerce/ попали в wheel как ecommerce/.
Иерархия сохранена, main.py внутрь не попал (это скрипт-запускатор,
не часть пакета).
3. Сборка через backend — потрошим wheel#
%%bash
cd demo-project
rm -rf dist/
uv build 2>&1 | tail -5
ls -la dist/
Building source distribution...
Building wheel from source distribution...
Successfu
lly built dist/greet-0.1.0.tar.gz
Successfully built dist/greet-0.1.0-py3
-none-any.whl
total 800
drwxr-xr-x 5 chopik staff 160 Sep 7 08:07 .
drwxr-xr-x 10 chopik staf
f 320 Sep 7 08:07 ..
-rw-r--r-- 1 chopik staff 1 Sep 7 08:07 .gitignore
-r
w-r--r-- 1 chopik staff 1813 Sep 7 08:07 greet-0.1.0-py3-none-any.whl
-rw-r--r-- 1 chopik
staff 400942 Sep 7 08:07 greet-0.1.0.tar.gz
Wheel — это ZIP. Смотрим через zipfile из stdlib (никакого unzip):
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)
greet/__init__.py
greet/cli.py
greet-0.1.0.dist-info/METADATA
greet-0.1.0.dist-info/WHEEL
greet-0.1.0.dist-info/entry_points.txt
greet-0.1.0.dist-info/RECORD
Что за dist-info/* файлы:
METADATA — плоский формат
[project](RFC 5322, headers-like).WHEEL — версия формата + platform tags.
RECORD — CSV
path,hash,sizeдля integrity при uninstall.entry_points.txt — если есть
[project.scripts].
METADATA изнутри:
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())
Metadata-Version: 2.5
Name: greet
Version: 0.1.0
Summary: Тривиальный пакет для демонстрации packaging
Author-email: Иван Лущ <ch0p1k3@yandex.ru>
License: MIT
Requires-Python: >=3.14
Provides-Extra: lint
Requires-Dist: mypy>=1.13; extra == 'lint'
Requires-Dist: ruff>=0.8; extra == 'lint'
Provides-Extra: test
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown
# greet — demo-пакет для лекции 1
Тривиальный пакет для демонстрации PEP 517/518/621, sdist vs wheel,
extras, ruff, mypy strict.
## Локально
```bash
uv sync --extra test --extra lint
uv run pytest
uv run ruff check .
uv run mypy --strict src/
```
Ту же самую информацию pip читает при pip install — без запуска
никакого Python-кода.
Что в sdist:
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)
greet-0.1.0/uv.lock
greet-0.1.0/dist-a/greet-0.1.0-py3-none-any.whl
greet-0.1.0/dist-a/greet-0.1.0.tar.gz
greet-0.1.0/dist-b/.gitignore
greet-0.1.0/dist-b/greet-0.1.0-py3-none-any.whl
greet-0.1.0/dist-b/greet-0.1.0.tar.gz
greet-0.1.0/src/greet/__init__.py
greet-0.1.0/src/greet/cli.py
greet-0.1.0/tests/test_greet.py
greet-0.1.0/.gitignore
greet-0.1.0/README.md
greet-0.1.0/pyproject.toml
greet-0.1.0/PKG-INFO
Внутри sdist — исходники (в отличие от wheel где только собранные .py
плюс metadata).
PEP 517 build protocol вживую#
Что делает uv build под капотом:
Читает
[build-system]— видитhatchling.build.Ставит
hatchlingв изолированный build-env.import hatchling.build; hatchling.build.build_wheel(...).Backend собирает файлы, кладёт wheel в
dist/.
Смотрим шаги через pip install -v:
%%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
Building wheels for collected packages: greet
Successfully built greet
Installing collected packages
: greet
Successfully installed greet-0.1.0
4. PyPI — куда всё это едет#
PyPI (pypi.org) — центральный индекс Python-пакетов. ~600k пакетов в 2026, ~1M загрузок в секунду. Основан 2003, ранее назывался «cheese shop» (Monty Python отсылка).
Когда пишешь pip install requests — pip идёт именно сюда. Как
именно? Через Simple API (PEP 503):
%%bash
# Simple API — просто HTML-страница со списком wheels пакета
curl -s https://pypi.org/simple/requests/ | head -20
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="pypi:repository-version" content="1.4">
<m
eta name="pypi:project-status" content="active"> <title>Links for requests</title>
</head>
<b
ody>
<h1>Links for requests</h1>
<a href="https://files.pythonhosted.org/packages/ba/bb/dfa0141a
32d773c47e4dede1a617c59a23b74dd302e449cf85413fc96bc4/requests-0.2.0.tar.gz#sha256=813202ace4d9301a3c
00740c700e012fb9f3f8c73ddcfe02ab558a8df6f175fd" >requests-0.2.0.tar.gz</a><br />
<a href="https://fi
les.pythonhosted.org/packages/4b/ad/d536b2e572e843fda13e4458c67f937b05ce359722c1e4cdad35ba05b6e3/req
uests-0.2.1.tar.gz#sha256=d54eb33499f018fc6bd297613bf866f8d134629c8e02964aab6ef951f460e41e" >request
s-0.2.1.tar.gz</a><br />
<a href="https://files.pythonhosted.org/packages/82/3c/3b5beca192da920c0c2b
a67119d66ba1e4b1e766f40898e5e684d697ca1c/requests-0.2.2.tar.gz#sha256=b3289694b2ddf6adb4f7e1f470b977
1330c76125611222b9c702f0e2e9733cbc" >requests-0.2.2.tar.gz</a><br />
<a href="https://files.pythonho
sted.org/packages/6f/7e/5c2d7d9102c6ab847bd1215f96255e894fbfc81c8abf2c1714ae2a504913/requests-0.2.3.
tar.gz#sha256=8e374b75aaae7f85325e9bb126e96cb77a3bfc17e81ee74a0e96916aac1cc2ba" >requests-0.2.3.tar.
gz</a><br />
<a href="https://files.pythonhosted.org/packages/dc/02/789859c27162bb91ecf6b72ed4ce1af3
ed1710255265ad0901c4d4e25666/requests-0.2.4.tar.gz#sha256=ef1bd1a81022e9bf574ecfe69cbd8597e79371b890
d29bd3847dd946102c8eed" >requests-0.2.4.tar.gz</a><br />
<a href="https://files.pythonhosted.org/pac
kages/96/2b/88e9d6bf2e9d75cda77bf4fdc03720f4ba262beb532f9510a4a7f3e45660/requests-0.3.0.tar.gz#sha25
6=57eed745eb2a2e3c7e1dd935ccd49eb2eac51cfcdace4a97fb44de5da70f0035" >requests-0.3.0.tar.gz</a><br />
<a href="https://files.pythonhosted.org/packages/5e/c0/76fac9445cd8b6394eacae1e098ca0c97767cc0112e4
5e68521f553df003/requests-0.3.1.tar.gz#sha256=05dddfd656d25b7738778d2b4e8fa72e53b5357a2f80a319e6e1fa
59edb03339" >requests-0.3.1.tar.gz</a><br />
<a href="https://files.pythonhosted.org/packages/d5/f1/
16b57088f11cd5c6c82834bad6475826309cee44edaae860e9f65c084703/requests-0.3.2.tar.gz#sha256=78ecf812ee
865b62be106100a3c6f24058c7901ad995351b8818f18ea97ce848" >requests-0.3.2.tar.gz</a><br />
<a href="ht
tps://files.pythonhosted.org/packages/f1/64/8a2ba81294381bb90e8fb4b6fa750e0dca3f2d19e8caaeeae5e7bb6b
3753/requests-0.3.3.tar.gz#sha256=ccbbc41c4c009baecf41e993727048c65c440fefadb217b11e73f63cd0cae09a"
>requests-0.3.3.tar.gz</a><br />
<a href="https://files.pythonhosted.org/packages/ed/1b/8682a0cfe92f
67e30fb9ac7982cb785a1230ca4385dc1353513f5b87b9f4/requests-0.3.4.tar.gz#sha256=e72a42a0317f33114b48c9
72d3056bad3265b92450d4e0e51ad0b384e43bc6d9" >requests-0.3.4.tar.gz</a><br />
<a href="https://files.
pythonhosted.org/packages/56/c3/0887d5d6c18a366308b3dc7024210b4c89ff9ae92ae5fb87cf8fe58bcae2/request
s-0.4.0.tar.gz#sha256=35185852569456de25a654c5f9a43a1b8e4dc18a2a676985bbb9d5e7e5a9703e" >requests-0.
4.0.tar.gz</a><br />
<a href="https://files.pythonhosted.org/packages/b3/54/dbc9b89a66a15ab9f3e2595d
e1b1ebd1da954efcb30a329c98710e014c05/requests-0.4.1.tar.gz#sha256=f978616765803e9e0e9943136b34be0da6
9d74ba8fbd064cbfcf28f33ca54d8a" >requests-0.4.1.tar.gz</a><br />
Список ссылок на wheels и sdist. Pip качает эту страницу, находит самую свежую версию, выбирает wheel под твою платформу, качает.
JSON API — детально про пакет#
%%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')
"
name: requests
version: 2.34.2
requires_python: >=3.10
license: Apache-2.0
summary: Python HTTP for
Humans.
project_urls: ['Documentation', 'Source']
files (latest): 2 вариантов wheel/sdist
Как pip выбирает wheel#
Смотрим что pip скачал бы для нас, без установки:
%%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/
bash: line 3: pip: command not found
Выбрал requests-*.whl под нашу платформу (py3-none-any — pure
Python, работает везде).
Публикация — обратный путь#
Мы пока ничего не публикуем, но flow тот же для всех:
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) — sandbox для проверки перед прод:
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 из коробки:
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:
# Последний коммит 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.
Работает и в pyproject.toml.dependencies, и на CLI, и в requirements.txt.
Через tarball / архив URL:
# 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:
pip install ./demo-project # обычная установка
pip install -e ./demo-project # editable
pip install ./demo-project[test,lint] # с extras
Именно это делает pip install . — берёт . как local path.
Воспроизводимость сборки: фиксируй commit hash
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 с тегом — компромисс (тег теоретически можно
перевесить, но обычно не двигают).
Security#
Typosquatting
Пакет requests — настоящий, официальный. А вот request (без -s),
requeests, python-requests — вредоносные копии с похожим именем. Всегда сверяй имя перед pip install.
Реальные инциденты: colourama (2017, вместо colorama), python-dateutil копии,
десятки в 2020-2024. pip install = выполнение произвольного кода от
автора пакета — доверяй только известным.
Как защититься:
pip install --require-hashes+requirements.txtс--hash=sha256:...— pip сверит SHA256 wheel’а с зафиксированным. Тот же принцип уuv.lock.Подписанные пакеты (PEP 458 — в процессе внедрения) — TUF-based подписи, работа PSF/Warehouse.
pip install .из непроверенного sdist = запуск чужогоsetup.py. Это одна из причин ухода от executablesetup.pyк декларативномуpyproject.toml.
Полезные команды#
!pip show requests 2>&1 | head -8
fish: Unknown command: pip
fish:
pip show requests 2>&1 | head -8
^~^
pip show <name> — локально установленная версия + metadata + место
(Location: ...).
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 шли из этого каталога:
%%bash
python3 -m venv /tmp/example-venv
source /tmp/example-venv/bin/activate
which python pip
python -c "import sys; print(sys.prefix)"
/tmp/example-venv/bin/python
/tmp/example-venv/bin/pip
/private/tmp/example-venv
Внутри активированного 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 (классика)#
%%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
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/venv-pip/bin/python3.14 -m pip install --upgrade pip
Package Version Editable project location
----------------- ------- ----------------------
---------------------------------------------------------------------------
ast_serialize 0.9.0
greet 0.1.0 /Users/chopik/advanced-python-ami-lectures-2026/course-content/lectures/01
-packaging/demo-project
iniconfig 2.3.0
librt 0.15.0
mypy 2.3.1
myp
y_extensions 1.1.0
packaging 26.3
pathspec 1.1.1
uv (современно)#
%%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
Using CPython 3.14.5 interpreter at: /opt/homebrew/opt/python@3.14/bin/python3.14
Creating virtual environment at: /tmp/venv-uv
Activate with: source /tmp/venv-uv/bin/activate.fish
Using Python 3.14.5 environment at: /private/tmp/venv-uv
Package Version Editable project location
----------------- ------- ----------------------
---------------------------------------------------------------------------
ast-serialize 0.9.0
greet 0.1.0 /Users/chopik/advanced-python-ami-lectures-2026/course-content/lectures/01
-packaging/demo-project
iniconfig 2.3.0
librt 0.15.0
mypy 2.3.1
myp
y-extensions 1.1.0
packaging 26.3
pathspec 1.1.1
В курсе — оба
Проверяющий скрипт использует pip install .[test], интерфейс общий —
подходит и pip-venv, и uv-venv. Выбирайте что удобнее; uv быстрее,
pip без внешних зависимостей.
Editable install: зачем и почему#
Показываем на живом примере что не так с обычным pip install . для
активной разработки.
Шаг 1. Копируем demo-project во временную директорию (чтобы экспериментировать не портя оригинал), ставим обычным способом, проверяем что работает:
%%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'))"
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/venv-no-e/bin/python3.14 -m pip install --upgrade pip
Hello, world!
Шаг 2. Правим исходник — меняем Hello, на HI,:
!perl -i -pe 's/Hello,/HI,/' /tmp/greet-exp/src/greet/__init__.py
!cat /tmp/greet-exp/src/greet/__init__.py
def greet(name: str) -> str:
return f"HI, {name}!"
Source изменился. Но:
!/tmp/venv-no-e/bin/python -c "from greet import greet; print(greet('world'))"
Hello, world!
По-прежнему Hello, world! — потому что pip install . скопировал
код в site-packages/ при установке, там лежит старая версия. Наш
edit в source-каталоге к установленному пакету не относится.
Шаг 3. Что можно сделать — переустановить:
%%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'))"
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/venv-no-e/bin/python3.14 -m pip install --upgrade pip
HI, world!
Теперь HI, world!. Но представь что цикл «правка → тест» происходит
каждые 30 секунд — каждый раз переустанавливать невыносимо.
Лечение — -e#
pip install -e /path — editable install. Пакет как бы «ставится»,
но site-packages/ содержит не копию, а ссылку на source-каталог.
import идёт напрямую в твои файлы.
%%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'))"
[notice] A new release of pip is available: 2
6.1.1 -> 26.2.1
[notice] To up
date, run: /private/tmp/venv-e/bin/python3.14 -m pip install --upgrade pip
HI, world!
Правим source ещё раз — теперь HI, -> HEY,:
!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!
Сразу видит HEY, world! — никакой переустановки. Именно то что
нужно для активной разработки.
Когда использовать
Разработка своего пакета — всегда
-e, чтобы правки не требовали переустановки.Дебаг чужого форка —
-e ~/forks/somelib+ правки прямо в клоне.Не для прода — deploy всегда обычный
pip install .(без-e).
Под капотом: .pth файл (PEP 660)#
Механизм — файл .pth в site-packages/. Python при старте читает
все *.pth в этом каталоге и добавляет каждую строку как путь в
sys.path. У нас в venv-e:
!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.
%%bash
cd demo-project
rm -f uv.lock
uv lock --quiet 2>&1 || true
head -25 uv.lock 2>/dev/null | head -20
version = 1
revision = 3
requires-python = ">=3.14"
resolution-markers = [
"python_full_version
>= '3.15'",
"python_full_version < '3.15'",
]
[[package]]
name = "ast-serialize"
version = "0.9
.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.or
g/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.ta
r.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 9512
93, upload-time = "2026-09-02T15:50:45.078Z" }
wheels = [
{ url = "https://files.pythonhosted.or
g/packages/0e/76/497f19d9bdb3899a1efd82e2957f455d0c6e0cb9ebbc254735acb1f74235/ast_serialize-0.9.0-cp
314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ae1c46eb97865823f9843c4b80145e011874923e1a
4a44b45738a5309d83e9f5", size = 889442, upload-time = "2026-09-02T15:49:21.144Z" },
{ url = "htt
ps://files.pythonhosted.org/packages/2d/c5/9fb64b7106c5534739322c74be7b743c4f2e3b5fd05d5b8e677f05c54
d5f/ast_serialize-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af082cb7e6c4fa3a428aa6
16c13d709a944076eba84a73184de15621cc1a915d", size = 1226721, upload-time = "2026-09-02T15:49:22.612Z
" },
{ url = "https://files.pythonhosted.org/packages/27/67/b550fc81aa0133808410783c6d9a1b925e31
610d226e836e21337850af55/ast_serialize-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e9
f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7", size = 1207369, upload-time = "2026-
09-02T15:49:23.916Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1e/ed9e66deb7da63e44
d0c0fd3a8feef698882ed56ea521a29494d4616eb46/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.
manylinux2014_aarch64.whl", hash = "sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2
b4c3da4", size = 1273073, upload-time = "2026-09-02T15:49:25.336Z" },
{ url = "https://files.pyt
honhosted.org/packages/68/8f/cd337551d7a68c982425bbf91f183943d7ccc62394002c74807a7f0e60db/ast_serial
ize-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b559cffac5a71
a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e", size = 1279045, upload-time = "2026-09-02T15:4
9:26.75Z" },
{ url = "https://files.pythonhosted.org/packages/40/c6/98dc41eb4122d5da83241e805739
838ed59e1e1b9006cbed89ded635a17f/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux20
14_ppc64le.whl", hash = "sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f", s
ize = 1539300, upload-time = "2026-09-02T15:49:28.213Z" },
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.
!uv tool run --with poetry poetry --version
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0/0
⠙ Resolving dependencies...
⠋ Resolving dependencies...
⠙ Resolving dependencies...
⠙ poetry==2.4.3
⠙ poetry-core==2.4.0
⠙ build==1.6.0
⠙ cachecontrol==0.14.4
⠙ cachecontrol==0.14.4
⠙ cleo==2.1.0
⠙ dulwich==1.2.14
⠙ fastjsonschema==2.22.2
⠙ findpython==0.8.0
⠙ installer==1.0.1
⠙ keyring==25.7.0
⠙ packaging==26.3
⠙ pbs-installer==2026.9.1
⠙ pbs-installer==2026.9.1
⠙ pbs-installer==2026.9.1
⠙ pkginfo==1.13
⠙ platformdirs==4.11.7
⠙ pyproject-hooks==1.2.0
Poetry (version 2.4.3)
Hatch (2022, Ofek Lev / PSF core) — full workflow, полностью
PEP 621. Сильная сторона — матрица окружений: в [tool.hatch.envs]
описываешь несколько сред (default, test, docs, …), в каждой
свой набор зависимостей и Python-версия, дальше hatch run test:pytest
прогоняет тесты в каждой из них. Аналог tox из старых времён
(старый инструмент того же класса, до сих пор используется).
!uv tool run --with hatch hatch --version
⠋ Resolving dependencies...
⠙ Resolving dependencies...
⠋ Resolving dependencies...
⠙ Resolving dependencies...
⠙ hatch==1.18.0
⠙ click==8.5.0
⠙ hatchling==1.32.0
⠙ httpx2==2.12.0
⠙ httpcore2==2.12.0
⠙ httpcore2==2.12.0
⠙ hyperlink==21.0.0
⠙ keyring==25.7.0
⠙ packaging==26.3
⠙ pexpect==4.9.0
⠙ platformdirs==4.11.7
⠙ pyproject-hooks==1.2.0
⠙ python-discovery==1.6.0
⠙ rich==15.0.0
⠙ shellingham==1.5.4
⠙ tomli-w==1.2.0
⠙ tomlkit==0.15.1
⠙ userpath==1.9.2
Hatch, version 1.18.0
PDM (2020, Frost Ming) — начинался с эксперимента PEP 582
(__pypackages__ вместо venv, идея как node_modules). Сейчас — обычный
CLI + lock, PEP 621 native.
!uv tool run --with pdm pdm --version
⠋ Resolving dependencies...
⠙ Resolving dependencies...
⠋ Resolving dependencies...
⠙ Resolving dependencies...
⠙ pdm==2.29.0
⠙ argcomplete==3.7.2
⠙ blinker==1.9.0
⠙ packaging==26.3
⠙ platformdirs==4.11.7
⠙ rich==15.0.0
⠙ virtualenv==21.7.8
⠙ pyproject-hooks==1.2.0
⠙ unearth==0.18.3
⠙ dep-logic==0.7.2
⠙ findpython==0.8.0
⠙ tomlkit==0.15.1
⠙ shellingham==1.5.4
⠙ python-dotenv==1.2.3
⠙ resolvelib==1.2.1
⠙ installer==1.0.1
⠙ truststore==0.10.4
⠙ hishel==1.3.1
PDM, version 2.29.0
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 |
|
|
|
|
|
Add dep |
|
|
|
|
|
Install |
|
|
|
|
|
Lock |
|
|
нет своего |
|
|
Run |
|
|
|
|
|
Publish |
|
|
|
|
|
Poetry lock несовместим
poetry.lock — только poetry читает. hatch/pdm/uv/pip не понимают.
То же обратно — poetry не читает чужие lock. Значит перейти с poetry на
что-то другое = потерять lock, перегенерировать заново.
Poetry ещё встречает [tool.poetry]
Legacy poetry-проекты используют свой [tool.poetry] вместо стандартного
[project] PEP 621. Внешне похоже, но синтаксис зависимостей отличается
(^1.0 вместо >=1.0,<2.0 и т.п.). Poetry 2+ поддерживает [project],
но старые проекты часто ещё на [tool.poetry].
Советы:
Новый проект —
uv. Один инструмент, быстрый, совместим сpip.Наследованный poetry — оставайся. Миграция редко того стоит.
hatch— хорошо для сложных проектов с несколькими environments (dev, docs, integration test с разными Python-версиями).pdm— нишевый. Единственная причина брать — если уже используешь.
Backend и manager — независимые оси
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.
6. Static tooling#
Два инструмента, оба с первого дня курса: ruff и mypy --strict.
ruff — линтер + форматтер#
Заменяет black + isort + flake8 + большинство плагинов, работает
быстро на любом размере кодовой базы.
Плохой код:
%%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)
Overwriting /tmp/bad.py
Линтер:
!uv run --no-project --with ruff ruff check /tmp/bad.py || true
]8;;https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line\E401]8;;\ [*] Multiple imports on one line
--> /tmp/bad.py:1:1
|
1 | import os,sys
| ^^^^^^^^^^^^^
2 | from typing import List, Dict
|
help: Split imports
|
- import os,sys
1 + import os
2 + import sys
3 | from typing import List, Dict
|
]8;;https://docs.astral.sh/ruff/rules/unsorted-imports\I001]8;;\ [*] Import block is un-sorted or un-formatted
--> /tmp/bad.py:1:1
|
1 | / import os,sys
2 | | from typing import List, Dict
| |______________________________^
3 |
4 | def greet( name:str )->str :
|
help: Organize imports
|
- import os,sys
- from typing import List, Dict
1 + import os
2 + import sys
3 + from typing import Dict, List
4 +
5 |
|
]8;;https://docs.astral.sh/ruff/rules/unused-import\F401]8;;\ [*] `os` imported but unused
--> /tmp/bad.py:1:8
|
1 | import os,sys
| ^^
2 | from typing import List, Dict
|
help: Remove unused import
|
- import os,sys
1 | from typing import List, Dict
|
]8;;https://docs.astral.sh/ruff/rules/unused-import\F401]8;;\ [*] `sys` imported but unused
--> /tmp/bad.py:1:11
|
1 | import os,sys
| ^^^
2 | from typing import List, Dict
|
help: Remove unused import
|
- import os,sys
1 | from typing import List, Dict
|
]8;;https://docs.astral.sh/ruff/rules/deprecated-import\UP035]8;;\ `typing.List` is deprecated, use `list` instead
--> /tmp/bad.py:2:1
|
1 | import os,sys
2 | from typing import List, Dict
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | def greet( name:str )->str :
|
]8;;https://docs.astral.sh/ruff/rules/deprecated-import\UP035]8;;\ `typing.Dict` is deprecated, use `dict` instead
--> /tmp/bad.py:2:1
|
1 | import os,sys
2 | from typing import List, Dict
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | def greet( name:str )->str :
|
]8;;https://docs.astral.sh/ruff/rules/unused-import\F401]8;;\ [*] `typing.List` imported but unused
--> /tmp/bad.py:2:21
|
1 | import os,sys
2 | from typing import List, Dict
| ^^^^
3 |
4 | def greet( name:str )->str :
|
help: Remove unused import
|
1 | import os,sys
- from typing import List, Dict
2 |
|
]8;;https://docs.astral.sh/ruff/rules/unused-import\F401]8;;\ [*] `typing.Dict` imported but unused
--> /tmp/bad.py:2:27
|
1 | import os,sys
2 | from typing import List, Dict
| ^^^^
3 |
4 | def greet( name:str )->str :
|
help: Remove unused import
|
1 | import os,sys
- from typing import List, Dict
2 |
|
Found 8 errors.
[*] 6 fixable with the `--fix` option.
Autofix:
!uv run --no-project --with ruff ruff check --fix /tmp/bad.py 2>&1 | tail -3
Found 5 errors (5 fixed, 0 remaining).
Форматтер (аналог black):
!uv run --no-project --with ruff ruff format /tmp/bad.py && cat /tmp/bad.py
1 file reformatted
def greet(name: str) -> str:
result = "Hello, " + name
return result
x = greet("world")
print(x)
Конфиг:
[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 в тестах
Не ставь select = ["ALL"]
Там есть правила, которые противоречат друг другу — один включённый запрещает то, что требует другой, и линтер начинает ругаться на любой код. Начинай с базового набора, расширяй по мере надобности.
mypy strict#
Без strict — mypy молчит на многом:
%%writefile /tmp/untyped.py
def greet(name):
return "Hi " + name
greet(42) # int вместо str - но mypy без strict пропустит
Overwriting /tmp/untyped.py
Тихо (--config-file=/dev/null — чтобы не подхватил конфиг курса
course-content/pyproject.toml, где strict уже включён):
!uv run --no-project --with mypy mypy --config-file=/dev/null /tmp/untyped.py 2>&1 | grep -v "No \[mypy\]" || true
Success: no issues found in 1 source file
Со strict — ловит:
!uv run --no-project --with mypy mypy --config-file=/dev/null --strict /tmp/untyped.py 2>&1 | grep -v "No \[mypy\]" || true
/tmp/untyped.py:1: error: Function is missing a type annotation [no-untyped-def]
/tmp/untyped.py:4: error: Call to untyped function "greet" in typed context [no-untyped-call]
Found 2 errors in 1 file (checked 1 source file)
Fix — типы:
%%writefile /tmp/typed.py
def greet(name: str) -> str:
return "Hi " + name
greet("world")
Overwriting /tmp/typed.py
!uv run --no-project --with mypy mypy --config-file=/dev/null --strict /tmp/typed.py 2>&1 | grep -v "No \[mypy\]"
Success: no issues found in 1 source file
Конфиг:
[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. Ставит git-hook,
который перед git commit прогоняет любые проверки. Пример
.pre-commit-config.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]
Установка:
uv pip install pre-commit
pre-commit install
В курсе не требуется
Мы не ставим pre-commit как часть шаблона задач: студенту это
не обязательно. Кто хочет, настраивает у себя в форке; коммитить
.pre-commit-config.yaml в публичный репо не нужно, это твой личный
workflow.

