Установка и инструментарий: версии, venv, pip, uv, poetry, линтеры
Первая глава любого курса по языку обычно выглядит как список команд: скачай, установи, проверь версию. С Python так не работает, и это не придирка — это следствие устройства языка. В Go есть один тулчейн, в Rust — один cargo, в Node — node_modules рядом с проектом. В Python нет ни того, ни другого: интерпретатор в системе общий, а import ищет модули не в каталоге проекта, а в глобальном списке путей внутри процесса. Весь зоопарк инструментов — venv, pip, uv, poetry, pipx, conda — существует ровно для того, чтобы управлять этим списком и тем, что в нём лежит.
Поэтому эта глава устроена не как инструкция, а как разбор модели. Сначала мы поймём, что такое «окружение» технически, и почти все загадочные ошибки («поставил, а он не видит», «работает у меня, падает в CI») перестанут быть загадочными. Потом наложим на модель инструменты и увидим, какой из них какой слой закрывает.
Если вы только начинаете программировать, идите не сюда, а в курс Программирование с нуля — он учит основам на Python и не требует ничего из написанного ниже. Здесь предполагается, что код вы уже пишете, и речь идёт о профессиональной работе с языком. Карта трека — в обзоре.
Модель: окружение — это sys.path
Когда вы пишете import requests, интерпретатор не «ищет установленные пакеты». Он последовательно перебирает каталоги из списка sys.path и берёт первое совпадение. Всё. Виртуальное окружение — это способ подсунуть интерпретатору другой sys.path; менеджер пакетов — способ положить файлы в нужный каталог этого списка.
import sys
print(sys.executable) # какой именно бинарник сейчас работает
print(sys.prefix) # корень активного окружения
print(sys.base_prefix) # корень базовой установки Python
print(sys.path) # порядок поиска модулей
# Единственная надёжная проверка «я в виртуальном окружении?»
in_venv = sys.prefix != sys.base_prefix
print("venv:", in_venv)
Типичный вывод внутри venv:
/home/dev/proj/.venv/bin/python
/home/dev/proj/.venv
/usr
['', '/usr/lib/python313.zip', '/usr/lib/python3.13',
'/usr/lib/python3.13/lib-dynload',
'/home/dev/proj/.venv/lib/python3.13/site-packages']
venv: True
Обратите внимание: стандартная библиотека приходит из /usr/lib/python3.13 — из базовой установки. Venv её не копирует, он только подменяет site-packages. Отсюда, кстати, следует, что venv нельзя «обновить» на новую минорную версию Python: пути внутри pyvenv.cfg и симлинк на бинарник указывают на конкретный 3.13, и после удаления системного 3.13 окружение превращается в тыкву. Правильная реакция — удалить и пересоздать.
Дальше три факта, из которых выводится почти вся практика:
sys.path[0]— каталог запущенного скрипта. Он идёт перед стандартной библиотекой. Файлrandom.pyрядом с вашимmain.pyсломает всё, что импортируетrandom, включая чужие библиотеки. С Python 3.11 есть флаг-P(и переменнаяPYTHONSAFEPATH=1), который это отключает.pip— это обычный пакет внутри какого-то окружения, а не системная утилита. КомандаpipвPATHможет относиться к совершенно другому интерпретатору, чем тотpython, который вы только что запустили. Поэтому канон —python -m pip: так гарантированно один и тот же интерпретатор.- Активация окружения не обязательна.
source .venv/bin/activateвсего лишь дописывает.venv/binв началоPATHи ставитVIRTUAL_ENV. Прямой вызов.venv/bin/pythonилиuv runдаёт ровно тот же результат — и это то, что нужно в скриптах, cron и Docker, где ритуал активации только добавляет способов ошибиться.
Какую версию Python ставить
С 2019 года CPython живёт по PEP 602: один мажорный релиз в год, в октябре. Каждая версия поддерживается пять лет — примерно два года исправлений ошибок и три года только security-патчей.
Практическое правило выбора:
- Приложение (сервис, скрипт, ETL) — берите текущий стабильный релиз или предыдущий. На момент написания это 3.13/3.14. Экономить на версии бессмысленно: 3.11 принёс 10–60% ускорения интерпретатора относительно 3.10 (проект Faster CPython), 3.12 и 3.13 добавили ещё.
- Библиотека — поддерживайте диапазон, который реально используют потребители: обычно
requires-python = ">=3.10". Каждая дополнительная старая версия — это отдельная колонка в матрице CI и запрет на новый синтаксис. - Не берите системный Python как рантайм проекта. Об этом отдельно ниже.
Что важного произошло в последних версиях с точки зрения инженера, а не списка фич:
| Версия | Что изменило работу |
|---|---|
| 3.11 | Крупное ускорение; ExceptionGroup и except*; tomllib в stdlib |
| 3.12 | Новый синтаксис дженериков def f[T](...); @override; f-строки без ограничений |
| 3.13 | Экспериментальная сборка без GIL (PEP 703); экспериментальный JIT (PEP 744); новый REPL |
| 3.14 | Free-threaded сборка официально поддерживается (PEP 779); ленивое вычисление аннотаций (PEP 649); t-строки; субинтерпретаторы в stdlib (PEP 734) |
Free-threaded сборка (python3.14t) — отдельный вариант интерпретатора, не замена обычного. Она не включена по умолчанию, часть C-расширений с ней несовместима, однопоточная производительность ниже. Подробно про это — в главе Конкурентность; здесь достаточно знать, что «Python убрал GIL» — заголовок новости, а не описание реальности 2026 года.
Откуда брать интерпретатор
Чего нельзя делать: ставить пакеты в системный Python
В Linux и macOS Python — часть операционной системы. apt, dnf, менеджеры пакетов и куски системных утилит написаны на нём и завязаны на конкретные версии библиотек в системном site-packages. sudo pip install туда — это способ незаметно сломать систему.
Начиная с PEP 668 дистрибутивы помечают такие установки файлом EXTERNALLY-MANAGED, и pip отказывается работать:
error: externally-managed-environment
× This environment is externally managed
Это не препятствие, которое нужно обойти. Флаг --break-system-packages назван честно: он ломает системные пакеты. Правильный ответ на такое сообщение — создать виртуальное окружение.
Варианты установки
в одном окружении"] H --> J["Создать .venv на проект"]
Разбор вариантов:
- python.org/downloads — официальные сборки. На Windows обязательно поставьте галочку «Add python.exe to PATH» и не отключайте
pylauncher: он умеетpy -3.12 -m venv .venvи решает половину проблем с несколькими версиями. uv python install 3.13— самый быстрый путь сегодня.uvскачивает готовые переносимые сборки (проект python-build-standalone), не требует компилятора и ставит версию за секунды. Умеетuv python pin 3.13, записывая версию в.python-version.- pyenv — классика. Собирает CPython из исходников, поэтому требует установленных заголовков (
libssl-dev,libffi-dev,zlib1g-devи т.д.) и нескольких минут на версию. Работает через shim-скрипты вPATH, что иногда конфликтует с другими инструментами. На Windows — отдельный форкpyenv-win. - deadsnakes PPA (Ubuntu) — свежие версии системными пакетами, ставятся рядом с системным Python, не заменяя его. Годится для серверов.
- conda / mamba / pixi — отдельная вселенная. Conda управляет не только Python-пакетами, но и нативными библиотеками (CUDA, MKL, GDAL, компиляторы). Если ваша работа — научные вычисления с тяжёлыми бинарными зависимостями, conda решает проблемы, которые pip не решает в принципе. Цена — свой формат, свои каналы и правило «не мешать
conda installиpip installв одном окружении». Подробнее в главе Работа с данными.
Полезная проверка после установки:
python -VV
# Python 3.13.2 (main, Feb 5 2026, 10:23:11) [GCC 13.2.0]
python -c "import sys, sysconfig; print(sys.base_prefix); print(sysconfig.get_config_var('CONFIG_ARGS'))"
Второй вызов показывает флаги сборки. Если там нет --enable-optimizations, интерпретатор собран без PGO/LTO и медленнее «правильной» сборки на 10–20% — типичная ситуация для быстрых сборок pyenv без PYTHON_CONFIGURE_OPTS.
Виртуальные окружения: venv и его устройство
Модуль venv (PEP 405) входит в стандартную библиотеку и делает удивительно мало:
python -m venv .venv # создать
python -m venv --upgrade-deps .venv # сразу свежие pip/setuptools
python -m venv --system-site-packages .venv # видеть ещё и глобальные пакеты (редко нужно)
python -m venv --without-pip .venv # окружение без pip: ставить будет uv
Внутри — симлинк на интерпретатор, пустой site-packages, скрипты активации и файл pyvenv.cfg:
home = /usr/bin
include-system-site-packages = false
version = 3.13.2
executable = /usr/bin/python3.13
command = /usr/bin/python3.13 -m venv /home/dev/proj/.venv
Именно pyvenv.cfg — весь механизм. При старте модуль site находит этот файл рядом с бинарником, читает home, выставляет sys.base_prefix на базовую установку, а sys.prefix — на каталог venv. Никакой магии, никакой изоляции на уровне ОС: процесс по-прежнему видит всю файловую систему.
Три следствия, о которых спрашивают чаще всего:
- Venv не переносим. В
bin/*лежат скрипты с абсолютным шебангом#!/home/dev/proj/.venv/bin/python. Переименовали каталог проекта — половина утилит перестала запускаться. Не копируйте.venvмежду машинами и не кладите в Docker-образ каталог, собранный на хосте: пересоздавайте. .venvне коммитится. В.gitignore— обязательно. Воспроизводимость обеспечивает лок-файл, а не каталог с бинарниками.- Один проект — одно окружение, имя
.venv. Это соглашение, которое понимают VS Code, PyCharm, uv, poetry и pytest. Экзотические имена стоят вам автоопределения интерпретатора в редакторе.
Страховка от установки мимо окружения:
export PIP_REQUIRE_VIRTUALENV=true # pip откажется работать вне venv
Что происходит при pip install
pip кажется простым, пока не начнёт вести себя странно. Внутри — четыре разных этапа, и ошибки на каждом выглядят по-разному.
Wheel против sdist
Имя wheel-файла — это метаданные, а не украшение:
numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
│ │ │ └── платформа: Linux с glibc ≥ 2.17, x86-64
│ │ └──────── ABI-тег: собран под конкретный ABI CPython 3.13
│ └────────────── версия Python: CPython 3.13
└───────────────────── версия пакета
Если для вашей связки «версия Python + ОС + архитектура» готового wheel нет, pip скачает sdist и попытается собрать пакет на месте — с компилятором C, заголовками Python и, для чего-нибудь вроде psycopg2, ещё и libpq-dev. Отсюда классические сюжеты:
- Вышел Python 3.14, а вы обновились в день релиза — половина бинарных пакетов ещё не выложила wheel под
cp314, и всё собирается из исходников или падает. - Alpine Linux использует musl вместо glibc, поэтому
manylinux-колёса не подходят.pip install pandasв alpine-образе компилирует numpy и pandas двадцать минут. Используйтеpython:3.13-slim(Debian) — образ на 30 МБ больше, зато сборка в секундах. - ARM-машина (Apple Silicon, Graviton) — проверяйте наличие
aarch64-колёс.
Тег abi3 (cp39-abi3-...) означает стабильный ABI: такой wheel работает на всех версиях Python начиная с указанной. Библиотеки вроде cryptography используют это, чтобы не пересобираться каждый октябрь.
Разрешение зависимостей
С версии 20.3 pip использует backtracking-резолвер: он честно перебирает комбинации версий, откатываясь при конфликте. Это правильно, но медленно, и при неразрешимом графе даёт ResolutionImpossible с длинной простынёй. Читать её нужно снизу вверх — там указаны конфликтующие требования.
python -m pip install "django>=5.0" "django-something==1.2"
# ...
# The conflict is caused by:
# The user requested django>=5.0
# django-something 1.2 depends on Django<5.0
Решение — не --no-deps (это откладывание взрыва), а либо обновление конфликтующего пакета, либо явное ослабление собственного требования.
Почему pip freeze > requirements.txt — плохая практика
Эта команда сваливает в один файл прямые и транзитивные зависимости, без различия. Через полгода никто в команде не знает, нужен ли six проекту напрямую или он приехал вместе с чем-то, что уже удалили. Плюс список привязан к платформе, на которой его сняли (pywin32 в файле от коллеги на Windows), и не содержит хешей.
Правильная схема — разделять намерение и результат:
# requirements.in — то, что мы действительно хотим (2-3 десятка строк)
# requirements.txt — результат разрешения, генерируется и коммитится
uv pip compile requirements.in -o requirements.txt --generate-hashes
uv pip sync requirements.txt # привести окружение ровно к файлу
Ключевое слово — sync, а не install. install только добавляет, sync ещё и удаляет лишнее, поэтому окружение сходится к описанному состоянию, а не накапливает исторический мусор.
Проверка целостности при установке:
python -m pip install --require-hashes -r requirements.txt
В этом режиме pip откажется ставить что-либо без явного хеша. Это защита цепочки поставок; подробнее — в главе Безопасность цепочки поставок.
pyproject.toml — единый файл конфигурации
До 2016 года проект на Python описывался исполняемым setup.py — то есть произвольным кодом, который приходилось запускать, чтобы узнать имя пакета. PEP 518 и PEP 621 заменили это декларативным TOML. Сегодня pyproject.toml — это одновременно манифест пакета и конфиг всех инструментов.
[project]
name = "orderflow"
version = "0.4.2"
description = "Сервис обработки заказов"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [{ name = "Team Orderflow", email = "dev@example.com" }]
# Прямые зависимости рантайма. Минимальные границы, а не точные версии:
# точные версии — работа лок-файла, а не манифеста.
dependencies = [
"fastapi>=0.115",
"pydantic>=2.9",
"httpx>=0.27",
"sqlalchemy>=2.0",
]
[project.optional-dependencies]
# Опциональные фичи, которые может запросить ПОТРЕБИТЕЛЬ: pip install orderflow[redis]
redis = ["redis>=5.0"]
[dependency-groups]
# PEP 735: группы для РАЗРАБОТКИ. В отличие от extras они не попадают в
# опубликованный пакет и не видны пользователям библиотеки.
dev = ["pytest>=8.3", "pytest-cov", "ruff>=0.9", "mypy>=1.13"]
docs = ["mkdocs-material"]
[project.scripts]
orderflow = "orderflow.cli:main" # создаст .venv/bin/orderflow
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "RUF", "PT", "S"]
ignore = ["E501"] # длину строк контролирует форматтер
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # assert в тестах — это нормально
[tool.pytest.ini_options]
addopts = "-q --strict-markers --cov=orderflow"
testpaths = ["tests"]
[tool.mypy]
python_version = "3.12"
strict = true
Разница между [project.optional-dependencies] и [dependency-groups] регулярно путается: extras — для пользователя пакета, группы — для разработчика пакета. Тестовые зависимости в extras — распространённая ошибка: они утекают в метаданные опубликованного дистрибутива.
Выбор build backend
| Backend | Когда брать |
|---|---|
hatchling |
Разумный дефолт: быстрый, декларативный, версия из файла или VCS |
setuptools |
Легаси-проекты, сложная сборка C-расширений, максимальная совместимость |
poetry-core |
Если проект уже живёт на poetry |
flit_core |
Крошечная чистая библиотека без сборочных шагов |
maturin |
Расширения на Rust (PyO3) |
scikit-build-core |
Расширения на C/C++ с CMake |
Лок-файлы и воспроизводимость
pyproject.toml говорит «нужен fastapi не ниже 0.115». Лок-файл говорит «сегодня ставим ровно 0.115.4, вот её хеш, вот 47 транзитивных пакетов с точными версиями». Первое — контракт, второе — снимок. Оба нужны, оба коммитятся.
До 2025 года стандарта на лок-файл не было: у poetry свой poetry.lock, у pdm — pdm.lock, у uv — uv.lock, у pip-tools — псевдо-requirements.txt. PEP 751 ввёл общий формат pylock.toml; поддержка постепенно появляется в инструментах (uv export --format pylock.toml, экспериментальная pip lock). До полной миграции живём с форматом своего менеджера.
Важный нюанс — кросс-платформенность лока. Разрешение может быть выполнено «для этой машины» или «для всех целевых платформ сразу» (universal resolution). uv по умолчанию делает второе: один uv.lock описывает Linux, macOS и Windows одновременно, с маркерами окружения из PEP 508. Если ваш лок сделан только под Linux, коллега на Mac получит другой набор пакетов — и «у меня работает» вернётся.
uv: сегодняшний дефолт
uv от Astral — менеджер, написанный на Rust, который закрывает роли pip + pip-tools + virtualenv + pyenv + pipx + poetry сразу. Быстрее pip обычно в 10–100 раз, и это не маркетинг: разница между «жду 40 секунд» и «жду 0.4 секунды» меняет то, как вы работаете.
Откуда скорость:
- Rust вместо Python и параллельные сетевые запросы вместо последовательных.
- Глобальный кеш распакованных пакетов; в
.venvкладутся жёсткие ссылки, а не копии. Создание окружения на 200 пакетов занимает доли секунды и почти не ест диск. - Резолвер на алгоритме PubGrub с внятными сообщениями о конфликтах.
- Метаданные версий берутся из индекса без скачивания архивов, где это возможно.
Рабочий цикл целиком:
uv init myservice && cd myservice # pyproject.toml, .python-version, скелет
uv python install 3.13 # поставить интерпретатор (без компиляции)
uv python pin 3.13 # зафиксировать версию для проекта
uv add fastapi "pydantic>=2.9" # добавить зависимость: правит pyproject + uv.lock + .venv
uv add --dev pytest ruff mypy # в группу dev (PEP 735)
uv remove httpx
uv lock # пересчитать лок, не трогая окружение
uv sync # привести .venv в точное соответствие локу
uv sync --frozen --no-dev # прод: не обновлять лок, без dev-зависимостей
uv run pytest # запуск в окружении проекта без активации
uv run --with rich python # разовый запуск с временной зависимостью
uv tool install ruff # утилита глобально, в своём изолированном venv
uvx ruff check . # запустить утилиту, ничего не устанавливая
uv build && uv publish # собрать wheel/sdist и выложить
uv pip install -r requirements.txt # слой совместимости со старыми проектами
Отдельная приятная вещь — однофайловые скрипты с зависимостями по PEP 723:
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "rich"]
# ///
import httpx
from rich import print
print(httpx.get("https://api.github.com/zen").text)
uv run fetch_zen.py # окружение создаётся на лету и кешируется
Это закрывает жанр «скрипт на сто строк, которому нужны две библиотеки» — раньше он требовал либо отдельного venv, либо грязной установки в систему.
Честно о минусах: uv молодой, ломающие изменения в мелких версиях случались, а uv.lock — формат одного вендора (пока pylock.toml не победил). Для команды это управляемый риск: uv export в любой момент выдаёт обычный requirements.txt, так что путь к отступлению есть всегда.
poetry: когда он всё ещё уместен
Poetry с 2018 года был тем инструментом, который приучил экосистему к нормальному локу и единому манифесту. Он жив, зрел и стоит в тысячах проектов.
poetry new myservice # скелет проекта
poetry add fastapi # зависимость + обновление poetry.lock
poetry add --group dev pytest
poetry install # поставить по локу
poetry install --sync # ещё и удалить лишнее
poetry run pytest
poetry build && poetry publish
poetry show --tree # дерево зависимостей
Poetry 2.x перешёл на стандартную секцию [project] из PEP 621 вместо собственной [tool.poetry] — это заметно снизило «особость» формата.
Когда выбирать poetry: проект уже на нём (миграция ради миграции не окупается); нужна устоявшаяся экосистема плагинов; команда не готова к инструменту, которому два года. Что раздражает: разрешение зависимостей на больших графах занимает минуты, poetry.lock порождает эпические конфликты в git при параллельных ветках, а собственный вертикальный стек (свой venv-менеджмент, свой резолвер, свой backend) труднее заменить по частям.
Отдельно: pipx — установка CLI-утилит (black, httpie, awscli) в собственные изолированные окружения, чтобы они не конфликтовали ни между собой, ни с проектами. uv tool install делает то же самое быстрее, но pipx по-прежнему нормальный выбор.
Линтеры и форматтеры
Историю тут проще показать, чем описать:
Сегодня практический ответ короткий: ruff. Один бинарник заменяет flake8 со всеми плагинами, isort, pyupgrade, pydocstyle, black (форматирование) и значительную часть pylint. Работает на весь монорепозиторий за десятые доли секунды, поэтому его не выносят «на потом», а держат в редакторе при сохранении.
ruff check . # линтинг
ruff check --fix . # автоисправление того, что чинится безопасно
ruff format . # форматирование (black-совместимое)
ruff check --statistics # сводка: какие правила срабатывают чаще всего
Наборы правил включаются префиксами; вот минимально осмысленный старт и что он даёт:
| Префикс | Источник | Что ловит |
|---|---|---|
E, W |
pycodestyle | Стилистика по PEP 8 |
F |
pyflakes | Неиспользуемые импорты и переменные, undefined names |
I |
isort | Порядок и группировка импортов |
UP |
pyupgrade | Устаревшие конструкции: typing.List → list и т.п. |
B |
flake8-bugbear | Реальные баги: мутабельные аргументы по умолчанию, except без типа |
SIM |
flake8-simplify | Упрощения: лишние if x: return True |
C4 |
comprehensions | list(x for x in y) → [x for x in y] |
S |
bandit | Проблемы безопасности: eval, subprocess(shell=True), слабые хеши |
PT |
pytest-style | Идиоматика фикстур и параметризации |
RUF |
ruff | Собственные диагностики, включая специфичные для Python |
Пример того, что находит B и почему это не придирка:
# Классическая ловушка Python: аргумент по умолчанию вычисляется ОДИН раз
# при определении функции, а не при каждом вызове.
def add_item(item, basket=[]): # ruff: B006 mutable-argument-default
basket.append(item)
return basket
print(add_item("яблоко")) # ['яблоко']
print(add_item("груша")) # ['яблоко', 'груша'] ← сюрприз
# Правильно:
def add_item(item, basket: list[str] | None = None) -> list[str]:
if basket is None:
basket = []
basket.append(item)
return basket
Эта и другие ловушки модели данных разбираются в главе Модель данных — линтер их только ловит, понимать причину всё равно нужно.
Чего ruff не делает: он не проверяет типы. Проверка типов — это mypy или pyright, отдельный инструмент с отдельной ценой внедрения; ей посвящена глава Аннотации типов. Ещё ruff пока не покрывает часть глубоких проверок pylint (анализ похожего кода, некоторые межмодульные проверки) — если они вам критичны, pylint можно оставить отдельным медленным шагом в CI.
Про black: он остаётся отличным форматтером и стандартом де-факто, но ruff format даёт совместимый результат в составе того же бинарника. Держать оба — лишняя сущность.
pre-commit: линтеры до коммита
pre-commit запускает проверки на git-хуках, причём только на изменённых файлах.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: ruff-check # в старых версиях хук назывался просто ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
- id: check-added-large-files
- id: detect-private-key # не даст закоммитить ключ
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.1
hooks:
- id: mypy
additional_dependencies: [pydantic, types-requests]
uv tool install pre-commit
pre-commit install # поставить git-хук
pre-commit run --all-files # прогнать по всему репозиторию (делайте при внедрении)
pre-commit autoupdate # обновить версии хуков
Две типичные ошибки. Первая: считать pre-commit заменой CI — его обходят через git commit --no-verify, поэтому те же проверки обязаны быть в пайплайне. Вторая: включить на существующем проекте и получить один коммит на 900 файлов; добавьте его хеш в .git-blame-ignore-revs, чтобы не портить git blame. Подробнее про хуки — в главе Git: хуки и автоматизация.
Всё вместе: CI
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Установить uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true # кеш глобального кеша uv между запусками
- name: Поставить нужный Python
run: uv python install ${{ matrix.python-version }}
- name: Синхронизировать окружение по локу
run: uv sync --frozen --all-extras --dev
# --frozen: упасть, если uv.lock не соответствует pyproject.toml.
# Это защита от «забыл перегенерировать лок».
- run: uv run ruff check --output-format=github .
- run: uv run ruff format --check .
- run: uv run mypy src
- run: uv run pytest --cov=src --cov-report=xml
- name: Аудит уязвимостей
run: uvx pip-audit
Ключевая идея — --frozen. Без него CI молча пересчитает зависимости, и вы получите «зелёный билд на других версиях, чем локально». Общая теория пайплайнов — в треке CI: основы.
Docker: собираем правильно
# --- Стадия сборки ---
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
WORKDIR /app
# Зависимости отдельным слоем: он пересобирается, только когда
# меняются pyproject.toml или uv.lock, а не при каждой правке кода.
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# --- Финальный образ: без uv и без инструментов сборки ---
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/.venv/bin:$PATH"
RUN useradd --create-home --uid 10001 app
COPY --from=builder --chown=app:app /app /app
USER app
WORKDIR /app
CMD ["uvicorn", "orderflow.main:app", "--host", "0.0.0.0", "--port", "8000"]
Разбор неочевидных мест:
PYTHONUNBUFFERED=1— иначеstdoutбуферизуется, и логи появляются в Kubernetes пачками с задержкой либо теряются при падении.PYTHONDONTWRITEBYTECODE=1в рантайме плюсUV_COMPILE_BYTECODE=1при сборке: байткод компилируется один раз в образ, а контейнер не пытается писать.pycв read-only файловую систему..venvвнутри образа лежит по фиксированному пути/app/.venvи добавлен вPATH— активация не нужна.- Непривилегированный пользователь — базовая гигиена; см. Контейнеры и реестры.
slim, а неalpine— из-за musl и отсутствияmanylinux-колёс, о чём выше.
Редактор
Минимальный набор, независимо от редактора: языковой сервер, форматтер по сохранению, отладчик.
- VS Code: расширения Python + Pylance (языковой сервер на движке pyright) + Ruff. Настройки:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.typeCheckingMode": "standard",
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": { "source.organizeImports.ruff": "explicit" }
}
}
- PyCharm — сильнее в рефакторингах, отладке и интеграции с БД; сам находит
.venvв корне проекта. - Neovim / Helix —
ruff server(LSP встроен в сам ruff, отдельныйruff-lspустарел) плюсbasedpyrightилиpyright.
Отладчик во всех случаях — debugpy; он же позволяет подключаться к процессу в контейнере. Сравнение редакторов — в треке Редакторы.
Грабли, на которых теряют дни
«Поставил пакет, а Python его не видит». В 95% случаев pip и python — из разных окружений. Диагностика: python -c "import sys; print(sys.executable)" и python -m pip --version (он печатает, к какому интерпретатору относится). Лечение — всегда python -m pip.
ModuleNotFoundError для стандартного модуля. Ищите файл с таким же именем в каталоге запуска: random.py, queue.py, email.py, types.py, test.py. sys.path[0] побеждает stdlib. Запустите с -P — если ошибка исчезла, диагноз подтверждён.
Странное поведение после переключения ветки. Осиротевшие .pyc в __pycache__ или каталог удалённого пакета. find . -name '__pycache__' -type d -exec rm -rf {} + и пересоздать окружение.
sudo pip install. Не делайте так никогда, даже если инструкция из интернета говорит обратное. Сообщение про externally-managed-environment — это не ошибка, а защита.
Обновление pip на Windows. pip install -U pip падает, потому что pip.exe занят собой. Работает только python -m pip install -U pip.
Тесты импортируют не то, что вы установили. Если пакет лежит в корне репозитория (flat layout), а не в src/, то при запуске pytest из корня импортируется локальный каталог, а не установленный пакет — и упаковочные ошибки (забытый файл в wheel) обнаружатся только у пользователей. Используйте src-layout; подробности — в главе Модули и пакеты.
PYTHONPATH как решение проблем импорта. Работает у вас, не работает нигде больше. Правильно — pip install -e . или uv sync, чтобы пакет был установлен по-настоящему.
Смешивание conda и pip. Conda не знает о том, что поставил pip, и при следующей операции может перезаписать файлы. Если без pip не обойтись — ставьте им в самом конце и фиксируйте окружение в environment.yml.
Незакреплённые версии в проде. requirements.txt вида fastapi без версии означает, что сборка сегодня и сборка завтра — разные приложения. Лок-файл обязателен.
Лок сгенерирован под одну платформу. Проверяйте, что менеджер делает universal-разрешение, иначе macOS-разработчик и Linux-CI живут на разных наборах пакетов.
Опечатка в имени пакета. PyPI полон typosquatting-пакетов (reqeusts, python-dateutils). Устанавливайте копированием имени из документации и держите в CI pip-audit.
Один venv на несколько проектов. «Чтобы не плодить каталоги» — и через месяц два проекта требуют несовместимых версий одной библиотеки. Диск дешевле времени.
Где Python-тулинг выигрывает, а где проигрывает
Честная оценка, без фанатизма.
Выигрывает. PyPI — крупнейший репозиторий пакетов в мире, и практически под любую задачу что-то уже есть. Формат wheel сделал распространение бинарных расширений реально работающим: pip install numpy ставит оптимизированную сборку с BLAS за секунды. Появление uv за два года закрыло главный разрыв со Go и Rust по скорости и удобству. Стандарты (PEP 517/621/735/751) наконец описывают весь конвейер, и инструменты стали взаимозаменяемыми.
Проигрывает. Единого благословлённого тулчейна нет и, вероятно, не будет: cargo и go идут в комплекте с языком, а в Python выбор менеджера — это решение команды, которое надо принимать и защищать. Нативные зависимости остаются источником боли на нестандартных платформах. Каждый проект несёт свой каталог .venv на сотни мегабайт. Стандарт лок-файла появился только в 2025 году — на 15 лет позже, чем у соседей. И главное: почти любой совет из интернета старше трёх лет теперь вреден, а поисковая выдача этого не сообщает.
Куда не стоит тащить. Если проекту нужна одна самодостаточная бинарная поставка без рантайма на целевой машине — Python здесь неудобен: PyInstaller и Nuitka работают, но это обходной путь, а не сильная сторона. Для встраиваемых систем с жёсткими ограничениями памяти и для задач, где важна предсказуемая латентность без GC-пауз, лучше посмотреть в сторону Go, Rust или C. Об этом честно и подробно — в главе Производительность.
Чек-лист рабочего окружения
- Интерпретатор поставлен не через системный пакетный менеджер как рантайм проекта; версия зафиксирована в
.python-version. - В каждом проекте свой
.venv, он в.gitignore. -
pyproject.tomlс секцией[project], dev-зависимости в[dependency-groups], а не в extras. - Лок-файл (
uv.lock/poetry.lock/pylock.toml) закоммичен; CI ставит с--frozen. -
ruff checkиruff format --checkзелёные локально и в CI. -
pre-commit installвыполнен; те же проверки продублированы в пайплайне. - Проверка типов настроена (см. главу 07) хотя бы в нестрогом режиме.
-
src-layout, тесты гоняются против установленного пакета. - Docker-образ многостадийный, на
slim, от непривилегированного пользователя, сPYTHONUNBUFFERED=1. - В CI есть
pip-auditили аналог; включён Dependabot/Renovate. - Никто в команде не пишет
sudo pip install.
Мини-итог
Окружение Python — это sys.path, а всё остальное надстройка над ним. Venv подменяет один каталог в этом списке, менеджер пакетов кладёт туда файлы, лок-файл фиксирует, какие именно. Слои упаковки стандартизированы PEP, поэтому инструменты взаимозаменяемы: pyproject.toml, wheel и site-packages одинаковы для pip, poetry и uv. Из этого следует и стратегия выбора: на новом проекте берите uv и ruff, на существующем не устраивайте миграцию ради моды, а в любом случае держите лок-файл, --frozen в CI и одно окружение на проект.
Источники
- Python Packaging User Guide — канонический источник по упаковке — https://packaging.python.org/
- The Python Tutorial: Virtual Environments and Packages — https://docs.python.org/3/tutorial/venv.html
venv— Creation of virtual environments — https://docs.python.org/3/library/venv.html- The import system — как на самом деле работает
import— https://docs.python.org/3/reference/import.html - pip documentation — https://pip.pypa.io/en/stable/
- uv documentation — https://docs.astral.sh/uv/
- Ruff documentation и список правил — https://docs.astral.sh/ruff/rules/
- Poetry documentation — https://python-poetry.org/docs/
- PEP 405 (venv), PEP 517/518 (сборка), PEP 621 (метаданные), PEP 668 (внешне управляемые окружения), PEP 723 (скрипты), PEP 735 (группы), PEP 751 (лок-файл) — https://peps.python.org/
- Brett Cannon. How virtual environments work — разбор от core-разработчика — https://snarky.ca/how-virtual-environments-work/
- Luciano Ramalho. Fluent Python, 2nd ed. (O’Reilly, 2022) — главная книга по идиоматичному Python — https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/
- Faster CPython — отчёты о работе над производительностью интерпретатора — https://github.com/faster-cpython/ideas
- pre-commit — https://pre-commit.com/
Что дальше
Инструменты на месте, окружение воспроизводимо. Теперь спустимся на уровень ниже — к тому, как Python устроен внутри: почему a is b и a == b отвечают по-разному, что на самом деле происходит при присваивании, почему изменяемые объекты ведут себя «странно» и как dunder-методы делают любой ваш класс полноправным гражданином языка.
Модель данных Python: объекты, ссылки, изменяемость, dunder-методы