Python Установка и инструментарий: версии, venv, pip, uv, poetry, линтеры
0%

Установка и инструментарий: версии, venv, pip, uv, poetry, линтеры

Установка и инструментарий: версии, 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 окружение превращается в тыкву. Правильная реакция — удалить и пересоздать.

Раскладка venv на диске и порядок поиска в sys.path

Дальше три факта, из которых выводится почти вся практика:

  1. sys.path[0] — каталог запущенного скрипта. Он идёт перед стандартной библиотекой. Файл random.py рядом с вашим main.py сломает всё, что импортирует random, включая чужие библиотеки. С Python 3.11 есть флаг -P (и переменная PYTHONSAFEPATH=1), который это отключает.
  2. pip — это обычный пакет внутри какого-то окружения, а не системная утилита. Команда pip в PATH может относиться к совершенно другому интерпретатору, чем тот python, который вы только что запустили. Поэтому канон — python -m pip: так гарантированно один и тот же интерпретатор.
  3. Активация окружения не обязательна. 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 назван честно: он ломает системные пакеты. Правильный ответ на такое сообщение — создать виртуальное окружение.

Варианты установки

Разбор вариантов:

  • python.org/downloads — официальные сборки. На Windows обязательно поставьте галочку «Add python.exe to PATH» и не отключайте py launcher: он умеет 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 — это одновременно манифест пакета и конфиг всех инструментов.

Слои упаковки Python и стандарты между ними

[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.Listlist и т.п.
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 / Helixruff 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 устроен внутри: почему a is b и a == b отвечают по-разному, что на самом деле происходит при присваивании, почему изменяемые объекты ведут себя «странно» и как dunder-методы делают любой ваш класс полноправным гражданином языка.

Модель данных Python: объекты, ссылки, изменяемость, dunder-методы

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов