Виртуальные среды и пакетные менеджеры
Модули и импорты
Прежде чем говорить о пакетных менеджерах, нужно разобраться в том, как Python вообще находит и загружает код. Без понимания механизма импортов невозможно осмысленно управлять зависимостями — ведь пакетный менеджер в конечном счёте просто кладёт файлы туда, где Python сможет их найти.
Модуль в Python — это файл с исходным кодом. Имя модуля доступно через __name__, а сам модуль является объектом и может быть присвоен переменной.
Синтаксис импорта
Python предлагает несколько способов импорта. Каждый из них — компромисс между удобством и явностью:
import numpy # полный доступ через numpy.xxx
numpy.array([1, 2, 3])
import numpy as np # псевдоним — короче, но всё ещё явно
np.array([1, 2, 3])
from numpy import array # конкретный объект — без префикса
array([1, 2, 3])
from numpy import * # всё публичное — НЕ рекомендуется!
__all__ — контроль экспорта
Если модуль определяет переменную __all__, то from module import * импортирует только то, что перечислено в этом списке:
__all__ = ['_hidden_variable']
int_variable = 1
_hidden_variable = 2
from module import *
print(_hidden_variable) # OK — есть в __all__
print(int_variable) # NameError! — нет в __all__
from module import * — плохая практика по нескольким причинам:
- Линтеры не видят, что именно импортируется, и не дают подсказок
- Можно случайно перезаписать существующую переменную в текущем пространстве имён
- По умолчанию не импортирует имена, начинающиеся с
_
Всегда используйте явные импорты. Если нужно экспортировать из модуля определённый набор — определите __all__.
Механизм импорта
Модули импортируются один раз — повторный import не перезагружает модуль. Это принципиально важно: если вы изменили файл модуля, но уже импортировали его, Python продолжит использовать старую версию:
import module
print(module.int_variable) # 1
module.int_variable += 1
import module # НЕ перезагрузит!
print(module.int_variable) # 2 — изменение осталось
import importlib
importlib.reload(module)
print(module.int_variable) # 1 — перезагружено из файла
importlib.reload() не работает с from module import variable — в этом случае переменная уже скопирована в текущее пространство имён и не обновится.
Jupyter Notebook и autoreload
В Jupyter есть магическая команда, которая автоматически перезагружает изменённые модули перед выполнением каждой ячейки:
%load_ext autoreload
%autoreload 2 # перезагружать ВСЕ изменённые модули
import my_module # теперь изменения подхватываются автоматически
Используйте %autoreload 2, а не %autoreload 1. Режим 1 перезагружает только модули, импортированные через %aimport, а 2 — все модули автоматически.
__name__ == "__main__" и запуск модуля как скрипта
Когда Python запускает файл напрямую (python fibo.py), переменная __name__ устанавливается в "__main__". Когда файл импортируется как модуль — в имя модуля. Это позволяет разделить код библиотеки и код запуска:
def fib(n):
a, b = 0, 1
for _ in range(n):
print(a, end=" ")
a, b = b, a + b
if __name__ == "__main__":
import sys
fib(int(sys.argv[1]))
Для более серьёзной обработки аргументов командной строки используйте библиотеку Click:
import click
@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello {name}!")
if __name__ == '__main__':
hello()
Typer — современная альтернатива Click, построенная поверх него. Typer использует type hints Python вместо декораторов, что делает код чище. Для новых проектов стоит рассмотреть именно его.
Поиск модулей
Python ищет модули в определённом порядке через список sys.path:
- Стандартная библиотека — встроенные модули (например,
sys,os) - Директория запускаемого скрипта — папка, в которой лежит
.pyфайл - Переменная окружения
PYTHONPATH— дополнительные пути, заданные пользователем - Директории site-packages — установленные сторонние библиотеки
import sys
for path in sys.path:
print(path)
sys.path — обычный список, его можно модифицировать в рантайме:
import sys
sys.path.append('../my_libs')
# теперь Python будет искать модули и в этой директории
Модификация sys.path в рантайме — хрупкий приём. Он работает, но делает проект зависимым от расположения файлов. Для production-кода лучше правильно настроить виртуальное окружение и установить пакет.
__pycache__ и .pyc файлы
При импорте модуля CPython проходит через несколько этапов проверки кэша:
После первого импорта модуля Python создаёт в его директории папк у __pycache__ с файлами .pyc — скомпилированным байт-кодом. Зачем это нужно?
- Быстрее импорт — не нужно заново парсить исходный код
- Скорость выполнения не меняется — исполняется тот же байт-код
- Файлы
.pycне зависят от платформы, но зависят от версии Python - Не нужно хранить в git (добавьте
__pycache__/в.gitignore)
Можно заранее скомпилировать все файлы в директории:
python -m compileall .
Формат хранения .pyc файлов описан в PEP 3147. До Python 3.2 файлы .pyc лежали рядом с исходниками, что создавало путаницу. Теперь они хранятся в __pycache__/ с именами вида module.cpython-312.pyc, что позволяет нескольким версиям Python сосуществовать.
Пакеты
Пакет — это директория с модулями, объединёнными общей логикой.
__init__.py — обычные пакеты
Файл __init__.py делает директорию пакетом. Он может быть пустым или содержать инициализационный код:
my_package/
├── __init__.py # выполняется при import my_package
├── module_a.py
└── subpackage/
├── __init__.py
└── module_b.py
Namespace packages (без __init__.py)
Если __init__.py отсутствует, Python распознает директорию как namespace package. Такие пакеты могут быть распределены по нескольким директориям — полезно для крупных проектов, где части пакета поставляются отдельно.
__main__.py — запуск пакета
Файл __main__.py позволяет запускать пакет целиком через python -m:
python -m tarfile # запустит tarfile/__main__.py
python -m my_package # запустит my_package/__main__.py
Обратите внимание: используется имя модуля без .py, а не путь к файлу. Python ищет модуль через sys.path, включая встроенные модули и site-packages.
__main__.py — стандартный способ предоставить CLI-интерфейс для пакета. Так работают python -m pytest, python -m pip, python -m json.tool и многие другие.
Как работают импорты
Понимание правил разрешения имён при импорте помогает избежать ошибок.
from package import item
Python следует такому порядку:
- Ищет
itemкак атрибут вpackage(определённый в__init__.py) - Если не нашёл — ищет
itemкак подмодуль внутри пакета - Если ничего не нашёл — выбрасывает
ImportError
import item.subitem.subsubitem
Каждый элемент кроме последнего должен быть пакетом. Последний может быть модулем или пакетом, но не может быть классом или функцией.
Относительные импорты
Внутри пакетов можно использовать относительные импорты с точками:
from . import echo # из текущего пакета (sound.effects)
from .. import formats # из родительского пакета (sound)
from ..filters import equalizer # из соседнего подпакета (sound.filters)
Относительные импорты работают только внутри пакетов. В top-level скрипте (который вы запускаете напрямую) использовать . и .. нельзя.
Циклические импорты
Циклические импорты — одна из самых неочевидных проблем Python. Рассмотрим пример:
from two import func_two
def func_one():
func_two()
from one import func_one
def do_work():
func_one()
def func_two():
print("Hello, world!")
from two import do_work
do_work()
$ python main.py
ImportError: cannot import name 'func_two' from partially initialized module 'two'
Почему это происходит? Разберём по шагам:
main.pyначинает выполнятьfrom two import do_work- Python начинает загружать
two.py— создаёт объект модуляtwoвsys.modules, но кодtwo.pyещё не дошёл до определенияfunc_two - Первая строка
two.py—from one import func_one— запускает загрузкуone.py - Первая строка
one.py—from two import func_two— Python находитtwoвsys.modules(он уже частично загружен), ноfunc_twoещё не определена (кодtwo.pyостановился на строке 1) ImportError!
Решение: заменить from X import Y на import X:
import two # вместо: from two import func_two
def func_one():
two.func_two() # вместо: func_two()
import one # вместо: from one import func_one
def do_work():
one.func_one() # вместо: func_one()
def func_two():
print("Hello, world!")
Почему это работает? При import two Python создаёт объект модуля two в sys.modules и продолжает его загружать. Когда позже (при вызове функции) выполняется two.func_two(), модуль уже полностью загружен. Ключевое отличие: from two import func_two пытается прочитать атрибут прямо сейчас, а two.func_two() — отложенный поиск атрибута в момент вызова функции.
dir() и builtins
Функция dir() — удобный инструмент для интроспекции:
# Без аргументов — все имена в текущей области
>>> dir()
['__builtins__', '__doc__', '__name__', ...]
# С модулем — все атрибуты модуля
>>> import os
>>> dir(os)
['CLD_CONTINUED', 'CLD_DUMPED', 'DirEntry', ...]
# С объектом — все поля и методы
>>> dir([1, 2, 3])
['__add__', '__class__', 'append', 'clear', 'copy', ...]
dir() не показывает встроенные функции. Их список — в модуле builtins:
import builtins
print(dir(builtins))
# ['ArithmeticError', 'AssertionError', ..., 'print', 'range', 'zip']
Виртуальные окружения

Виртуальные окружения решают фундаментальную проблему: разные проекты требуют разных версий одних и тех же библиотек. Без изоляции установка requests==2.31 для одного проекта сломает другой, которому нужен requests==2.25.
Но есть и более серьёзная причина. На Linux-системах Python используется самой ОС — менеджер пакетов, системные утилиты, инструменты администрирования. Установка или обновление библиотек в системный Python может буквально сломать операционную систему.
sudo pip install в системный Python — путь к поломке ОС. Подробнее: What are the risks of running sudo pip?
venv (встроенный)
Начиная с Python 3.3, в стандартную библиотеку входит модуль venv — минимальный инструмент для создания виртуальных окружений:
# Создание
python -m venv .venv
# Активация (Linux/macOS)
source .venv/bin/activate
# Активация (Windows)
.venv\Scripts\activate
# Деактивация
deactivate
virtualenv
Сторонний инструмент, который работает быст рее venv и предлагает дополнительные возможности: выбор конкретной версии Python, кеширование wheel-файлов, расширяемость через плагины:
pip install virtualenv
virtualenv .venv --python=python3.12
Всегда создавайте виртуальное окружение для каждого проекта. Основной интерпретатор держите чистым — без сторонних библиотек. Это привычка, которая избавит от множества проблем.
Пакетные менеджеры
Пакетный менеджер берёт на себя то, что вы не хотите (и не должны) делать вручную: отслеживание версий, кеширование дистрибутивов, компиляцию нативных расширений и поиск пакетов по индексам.
pip и requirements.txt
pip — стандартный менеджер пакетов Python, встроенный в любую установку:
pip install requests # установка последней версии
pip install requests==2.31.0 # конкретная версия
pip install -r requirements.txt # установка из файла
pip freeze > requirements.txt # экспорт установленных пакетов
pip uninstall requests # удаление
Как работает pip install
- Скачивает
.whl(wheel) файл с PyPI для вашей платформы - Разархивирует пакет и запускает
setup.py(или обрабатывает метаданные изpyproject.toml) - Рекурсивно устанавливает зависимости, указанные в метаданных пакета
- Если пакет требует компиляции C/C++ расширений — запускает компилятор (который должен быть установлен)
- Пакет становится доступен для импорта
pip freeze фиксирует все установленные пакеты, включая транзитивные зависимости. Это хрупкий подход: невозможно отличить прямые зависимости от транзитивных. При обновлении одной библиотеки вы рискуете получить конфликты. Для лучшего контроля используйте инструменты с lock-файлами (Poetry, uv).
Conda
Почему Conda?
Conda — не просто менеджер пакетов, а полноценная система управления окружениями. Её ключевые преимущества:
- Мультиязычность — управляет пакетами Python, C/C++, R, Fortran и других языков
- Бинарные пакеты — не требует компиляции на стороне пользователя
- Надёжное разрешение зависимостей — использует SAT-солвер, гарантирующий консистентность окружения
Разница между Conda и pip
| Характеристика | pip | Conda |
|---|---|---|
| Встроен в Python | Да | Нет |
| Типы пакетов | Только Python | Любые (Python, C, R...) |
| Разрешение зависимостей | Базовое | SAT-солвер |
| Количество пакето в | Огромное (PyPI) | Меньше, но растёт |
| Скорость | Быстрый | Медленнее (но libmamba ускоряет) |
| Может сломать окружение | Да | Нет (солвер не допустит) |
Дистрибутивы Anaconda
- Anaconda — полный дистрибутив с сотнями предустановленных библиотек
- Miniconda — минимальная версия (только conda + Python)
- Mamba — реализация conda на C++, значительно быстрее оригинала
- Micromamba — ещё более компактный вариант Mamba, один статический бинарник
Каналы (channels)
Conda ищет пакеты в каналах — репозиториях с предсобранными пакетами:
- anaconda (defaults) — базовые библиотеки, курируемые Anaconda Inc.
- conda-forge — крупнейший community-канал с тысячами пакетов
- bioconda — специ ализированный канал для биоинформатики
Каждый дополнительный канал увеличивает время разрешения зависимостей — солверу нужно рассмотреть больше вариантов пакетов.
Как Conda устанавливает пакеты
Процесс установки пакета в Conda принципиально отличается от pip:
- Скачивает
repodata.json— полный индекс всех пакетов из подключённых каналов - Формулирует задачу SAT — переводит требования пользователя и зависимости всех пакетов в булеву формулу
- Решает SAT — находит набор пакетов, который удовлетворяет всем ограничениям
- Устанавливает найденный набор
Начиная с conda 23.x, солвер по умолчанию — libmamba (написан на C++). Это тот же алгоритм, что и в Mamba, но встроенный прямо в conda. Разница в скорости по сравнению со старым Python-солвером — на порядки.
SAT-солвер: как это работает
SAT (Boolean satisfiability problem) — задача определить, можно ли подобрать значения булевых переменных так, чтобы формула стала истинной. Для произвольных формул в конъюнктивной нормальной форме это NP-полная задача (первая доказанная NP-полная задача, теорема Кука-Левина).
Conda формулирует установку пакетов как SAT: каждый пакет каждой версии — переменная, а зависимости — ограничения. Солвер приоритизирует решения по:
- Приоритету канала — defaults важнее conda-forge (если не переопределено)
- Версии пакета — новее лучше
- Номеру сборки — новее лучше
- Таймстемпу — при прочих равных побеждает более свежая сборка
После нахождения допустимых решений солвер минимизирует количество изменений в окружении и удаляет ненужные пакеты. Результат — гарантированно консистентный набор, который не сломает существующие зависимости.
Создание пакета для Conda
Conda-пакет создаётся на основе рецепта (recipe):
meta.yaml— метаданные, зависимости, описаниеbuild.sh— скрипт сборки для Linux/macOSbld.bat— скрипт сборки для Windows
Сборка запускается через conda-build. Более современная альтернатива — rattler-build, написанный на Rust с более простым форматом рецептов.
Poetry
Poetry — обёртка поверх pip + setuptools + venv + twine, объединяющая управление зависимостями, виртуальными окружениями и публикацию пакетов в одном инструменте.
pip install poetry
poetry new my-project # создать проект с структурой
poetry init # инициализировать в существующем проекте
poetry add requests # добавить зависимость
poetry add --group dev pytest # dev-зависимость
poetry install # установить все зависимости
poetry run python main.py # запуск в окружении Poetry
[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "My awesome project"
[tool.poetry.dependencies]
python = "^3.12"
requests = "^2.31"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
ruff = "^0.4"
Устаревший синтаксис [tool.poetry.dev-dependencies] всё ещё работает, но правильный современный способ — [tool.poetry.group.dev.dependencies]. Старый формат может быть удалён в будущих версиях Poetry.
Poetry создаёт poetry.lock — файл с точными версиями всех зависимостей (включая транзитивные). Всегда коммитьте poetry.lock в репозиторий — это гарантия воспроизводимой сборки.
Poetry хорошо подходит для web-проектов и библиотек с PyPI-зависимостями. Для проектов с бинарными или научными зависимостями (NumPy, SciPy, PyTorch) он не лучший выбор — эти пакеты часто требуют платформо-зависимой компиляции, которую conda/pixi решают лучше.
PDM
PDM — современный менеджер зависимостей, который строго сл едует стандартам PEP:
- PEP 621 — метаданные проекта в
[project](а не в проприетарном[tool.pdm]) - Быстрый резолвер зависимостей
- Гибкая система плагинов
- Пользовательские скрипты
PDM — хороший выбор, если важна строгая совместимость со стандартами Python. Функционально он похож на Poetry, но менее популярен.
uv
uv — менеджер пакетов нового поколения от Astral (создатели ruff). Написан на Rust и работает в 10-100 раз быстрее pip.
До uv существовал проект rye, созданный Армином Ронахером — автором Flask, Jinja2 и Click. Rye был экспериментальным инструментом для управления Python-проектами. В 2024 году команда Astral взяла rye под свою поддержку и полностью заменила его на uv. Rye больше не развивается — все пользователи переведены на uv.
uv — это «всё в одном»: управление версиями Python, виртуальные окружения, зависимости, сборка и публикация:
# Управление версиями Python
uv python install 3.12 # установить Python 3.12
uv python pin 3.12 # зафиксировать версию для проекта
# Создание проекта
uv init my-project # создать новый проект
uv add requests # добавить зависимость
uv add --dev pytest # dev-зависимость
# Запуск
uv run python main.py # запуск в виртуальном окружении
uv run pytest # запуск инструментов
# Инструменты (аналог pipx)
uv tool install ruff # установить CLI-инструмент глобально
uv tool run black . # запустить без установки
uv генерирует uv.lock — lock-файл с точными версиями всех зависимостей. Формат стандартизирован и позволяет экспортировать в requirements.txt:
uv export -o requirements.txt --no-hashes --no-dev --no-editable --no-emit-project
uv — рекомендуемый выбор для новых проектов с PyPI-зависимостями. Он заменяет pip, venv, pip-tools, pipx и частично poetry — одним быстрым инструментом.
pixi
pixi — менеджер проектов от prefix.dev, основанной бывшими разработчиками Mamba. Написан на Rust и объединяет лучшее из двух миров:
- Conda-пакеты — для бинарных, научных и платформо-зависимых библиотек (свой солвер на Rust)
- PyPI-пакеты — через uv под капотом
Ключевые особенности:
- Python — просто пакет (как в conda), что упрощает управление версиями
- Мультиплатформенная валидация — pixi проверяет, что зависимости доступны на всех указанных платформах, и сообщает о проблемах заранее
- Кросс-платформенные скрипты — через встроенный
deno_task_shell - pixi.lock — единый lock-файл для всех окружений и платформ
pixi пока не умеет собирать и публиковать пакеты. Модуль сборки находится в активной разработке. Это важное ограничение, если вы создаёте библиотеку для распространения.
Что выбрать?
Сравнение возможностей пакетных менеджеров:

Выбор инструмента зависит от типа проекта и зависимостей:
| Сценарий | Рекомендация |
|---|---|
| Библиотека с PyPI-зависимостями | Poetry, PDM или uv (рекомендуется) |
| Приложение для деплоя (только PyPI) | uv |
| Бинарные / научные зависимости | pixi |
| Библиотека с бинарными зависимостями | pixi (но build-модуль ещё в разработке) |
Сборка и публикация пакетов
Современный подход: pyproject.toml (PEP 517/518)
PEP 517 и PEP 518 стандартизировали систему сборки Python-пакетов. Файл pyproject.toml — единственный необходимый конфигурационный файл для современного проекта:
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
requires-python = ">=3.12"
description = "My awesome package"
dependencies = ["requests>=2.25"]
[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.4"]
build-backend должен быть "setuptools.build_meta", а не "setuptools.backends._legacy:_Backend". Вторая форма — внутренний API, который не предназначен для использования в pyproject.toml.
Сборка и публикация:
# Сборка — создаёт dist/ с .whl и .tar.gz
python -m build
# Публикация на тестовый PyPI
python -m twine upload --repository testpypi dist/*
# Публикация на production PyPI
python -m twine upload dist/*
Структура проекта:
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── core.py
└── tests/
└── test_core.py
Устаревший подход: setup.py
До pyproject.toml пакеты описывались через setup.py и setup.cfg:
from setuptools import setup, find_packages
setup(
name="my-package",
version="0.1.0",
packages=find_packages(),
install_requires=["requests>=2.25"],
)
setup.py и setup.cfg — устаревший подход. Они всё ещё работают, но для новых проектов используйте pyproject.toml. Все современные инструменты (uv, poetry, PDM, pip) полностью поддерживают PEP 517/518.
PEP 723 — встроенные метаданные скрипта
PEP 723 позволяет указать зависимости прямо внутри Python-скрипта. Это удобно для одиночных скриптов, которые не являются частью проекта:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests>=2.31",
# "rich>=13.0",
# ]
# ///
import requests
from rich import print
response = requests.get("https://api.github.com/zen")
print(f"[bold green]{response.text}[/bold green]")
uv автоматически создаёт временное окружение и устанавливает зависимости:
uv run fetch_data.py # скачает requests и rich, запустит скрипт
PEP 723 отлично подходит для утилитарных скриптов, которые хочется запускать одной командой без настройки окружения. Скрипт сам описывает свои зависимости — никакого requirements.txt не нужно.
PEP 735 — группы зависимостей
PEP 735 вводит стандартный (не tool-специфичный) способ описания групп зависимостей в pyproject.toml:
[dependency-groups]
test = ["pytest>=8.0", "coverage>=7.0"]
lint = ["ruff>=0.4", "mypy>=1.10"]
dev = [{include-group = "test"}, {include-group = "lint"}]
Преимущество — это кросс-инструментальный стандарт. Вместо [tool.poetry.group.dev.dependencies] или [project.optional-dependencies] используется единый формат, который понимают uv, pip, и другие инструменты.
Создание пакета для Conda
Conda-пакет создаётся из рецепта:
package:
name: my-package
version: "0.1.0"
source:
path: .
build:
number: 0
script: python -m pip install . --no-deps
requirements:
host:
- python >=3.12
- pip
- setuptools
run:
- python >=3.12
- requests >=2.25
about:
summary: My awesome package
# Классический способ
conda-build .
# Современная альтернатива (Rust, быстрее, проще)
rattler-build build --recipe recipe.yaml
rattler-build использует собственный формат рецептов (YAML, но с другой структурой), который проще и строже, чем meta.yaml от conda-build.
Полезные ссылки
- Модули в Python — официальная документация
__main__— Python docs- Packaging guide от Python.org
- PEP 3147 — .pyc Repository Directories
- PEP 517 — Build system interface
- PEP 518 — Build system requirements
- PEP 621 — Project metadata
- PEP 723 — Inline script metadata
- PEP 735 — Dependency Groups
- uv documentation
- pixi documentation
- Poetry documentation
- PDM documentation
- Click documentation
- Typer documentation
- Как работает pip
- Про то как решается SAT
- Управления пакетами Python: Хронология
- Conda: understanding solver performance
- Risks of sudo pip