Python з нуля · Блок 4 · Тема 17

Віртуальні середовища і pip

Два проєкти на одному комп'ютері хочуть різні версії однієї бібліотеки. Без окремих середовищ хтось із них обов'язково зламається — і це не гіпотеза, а розклад.

У темі 16 ми розклали програму по файлах і простежили, як import шукає модуль: спершу sys.modules, потім вбудовані, потім теки зі списку sys.path, остання з яких — site-packages. Саме туди складає файли pip. Тоді ми дійшли до межі стандартної бібліотеки й зупинилися на питанні: як узяти чужий пакет. Відповідь «встанови його» звучить просто рівно доти, доки проєкт у тебе один.

Ця тема — про те, що стається з другим проєктом. І про інструмент, який робить із «стається» звичайну нудну рутину з трьох команд.

01 / БільДва проєкти й одна тека

Уяви Олену. Торік вона написала «Афішу» — маленьку програму, що збирає розклад заходів. Афіша користується бібліотекою zvitnyk версії 1.4: там є функція zvit(dani), яка повертає готовий рядок, і Афіша одразу робить із ним .strip().

Цього тижня Олена почала новий проєкт — «Звіти». Він теж потребує zvitnyk, але версії 2.0: у другій версії автори переробили функцію, і тепер zvit(dani) повертає словник із розібраними полями. Звіти беруть звідти ["pidsumok"].

Обидва проєкти запускаються тим самим python3. Отже, обидва беруть пакети з однієї й тієї самої теки site-packages. А там zvitnyk може лежати рівно один: імпорт іде за іменем, а ім'я в пакета одне. Двох тек zvitnyk поруч не буває — друге встановлення просто затирає перше.

Покрути повзунок і подивись, що з цього виходить. Перемикач праворуч показує ту саму історію у двох світах: без ізоляції й з нею.

Інтерактив 1 · Дві версії, одна тека

Чотири дії по черзі. Стеж не за командою, а за станом обох проєктів унизу.

афіша
звіти
Що тут головне: у режимі «спільна тека» кількість щасливих проєктів ніколи не піднімається вище одного — кроки 1, 2 і 3 просто передають нещастя туди-сюди. Це не вада pip і не вада бібліотеки. Це наслідок того, що місце для пакетів одне на весь інтерпретатор.

Висновок напрошується сам: проєктам треба дати різні місця для пакетів. Не різні комп'ютери, не різні операційні системи — саме різні теки, з яких Python читатиме бібліотеки. Механізм, що це робить, зветься віртуальне середовище (virtual environment), а модуль для його створення — venv.

02 / ОзначенняЩо таке середовище фізично

Слово «віртуальне» звучить загадково, тому одразу приземлимо. Віртуальне середовище — це звичайна тека на диску. Усередині неї три речі, і жодної магії:

Чого в середовищі немає — теж важливо. Немає стандартної бібліотеки: усі ті math, json і pathlib беруться зі справжнього Python, на який вказує pyvenv.cfg. Тому порожнє середовище важить кілька мегабайтів, а не сотню, і створюється за секунди.

Це не віртуальна машина й не контейнер. Слова схожі, суть різна. Віртуальна машина емулює комп'ютер цілком, контейнер ізолює процеси й файлову систему. Віртуальне середовище не ізолює нічого, крім набору пакетів: та сама ОС, той самий процес, ті самі права доступу, той самий інтерпретатор. Це не пісочниця безпеки — програма з середовища може читати й видаляти твої файли так само вільно, як будь-яка інша.

Поклацай по вузлах дерева — праворуч побачиш, що це й навіщо:

Інтерактив 2 · Що всередині теки .venv

Реальна структура після python3 -m venv .venv. Обери файл або теку.

Найважливіший вузол — site-packages. Це та сама тека, про яку йшлося в темі 16 як про останній пункт sys.path. Різниця лише в тому, що тепер вона належить проєкту, а не всьому комп'ютеру. Усе інше в .venv існує заради того, щоб Python знайшов саме її.

03 / ПрактикаСтворити, активувати, вийти

Команда рівно одна, і ти вже бачила її форму в темі 02: прапорець -m означає «знайди модуль із такою назвою й виконай його як програму».

# стоячи в теці проєкту
python3 -m venv .venv

Кілька секунд — і поруч з'явилась тека .venv. Ім'я не обов'язкове, але це усталена домовленість: крапка на початку робить теку прихованою в Unix, а однакова назва в усіх проєктах дозволяє редакторам знаходити середовище автоматично. Одне середовище — один проєкт; ділити одне на два проєкти означає повернути собі рівно ту проблему, заради якої все й почалося.

Активація

Середовище створене, але оболонка про нього ще не знає. Активація — це запуск невеликого скрипта, який лежить усередині .venv. Команда залежить від оболонки, а не від настрою:

оболонкакоманда активаціїде це буває
bash / zshsource .venv/bin/activateLinux, macOS
fishsource .venv/bin/activate.fishLinux, macOS
PowerShell.venv\Scripts\Activate.ps1Windows
cmd.exe.venv\Scripts\activate.batWindows

Ознака успіху одна: перед запрошенням у терміналі з'явився префікс (.venv). Вихід — команда deactivate без аргументів; вона працює в будь-якій оболонці й повертає все як було. Закрити вікно термінала теж достатньо: активація живе рівно стільки, скільки живе цей процес оболонки.

Дві граблі Windows. Перша: PowerShell за замовчуванням відмовляється виконувати скрипти й каже running scripts is disabled on this system. Лікується один раз на весь профіль користувача: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned. Друга: у PowerShell не працює source — там просто пишуть шлях до скрипта, як у таблиці вище.
Середовище не можна переносити. Усередині .venv записані абсолютні шляхи — і в pyvenv.cfg, і в перших рядках виконуваних файлів pip. Перейменував теку проєкту — середовище зламалося. Це не трагедія: воно створюється за секунди й повністю відновлюється з requirements.txt, про який ми говоритимемо далі. Правильна реакція — видалити й створити заново, а не лагодити.

04 / МеханікаАктивація — це зсув PATH

Ось місце, де більшість людей уявляє собі щось містичне: «середовище ввімкнулося». Насправді скрипт активації робить рівно три дрібниці:

  1. дописує теку .venv/bin на початок змінної PATH;
  2. заводить змінну VIRTUAL_ENV зі шляхом до середовища — щоб інші програми могли про нього дізнатися;
  3. дописує (.venv) у рядок запрошення, щоб ти бачила, де перебуваєш.

Усе. Жодних змін у самому Python, жодного «режиму». Згадай схему з теми 02: коли ти пишеш слово в терміналі, оболонка проходить теки зі списку PATH згори вниз і бере перший збіг. Активація просто ставить теку середовища попереду всіх — і тепер перший збіг завжди її.

Інтерактив 3 · Куди дивиться оболонка після активації

Той самий пошук по PATH, що й у темі 02. Змінюється лише перший рядок списку.

що запуститься
Зверни увагу на python без трійки. Без середовища його на macOS і Linux зазвичай просто немає — саме те, що ми бачили в темі 02. Усередині середовища він з'являється й вказує куди треба. Ось чому коротка форма python у документації проєктів безпечна: там мають на увазі активоване середовище.
Чому активація робиться через source. Якби ти запустила activate як звичайну програму, вона стартувала б окремим процесом, змінила PATH у себе — і померла разом зі своїми змінами. Слово source означає «виконай цей файл у моїй оболонці, не породжуючи нову». Звідси й побічний ефект, який дивує: deactivate — не програма й не файл на диску. Це функція оболонки, яку створив скрипт активації. Пошук which deactivate нічого не знайде.

Активація не обов'язкова

Практичний наслідок, який рятує в скриптах і на сервері: якщо активація — це лише зручний спосіб знайти файл, його можна назвати й напряму.

.venv/bin/python zvit.py
↑ працює без жодної активації — узятий саме той інтерпретатор

.venv/bin/python -m pip install pandas
↑ і pip теж той, що треба

Саме так роблять у файлах автоматизації та в cron: там немає інтерактивної оболонки, і активувати нема кому. Форма -m pip замість голого pip знову з теми 02 — вона знімає всі питання про те, чий саме pip запустився.

05 / НаслідокЯк змінюється sys.path

Тепер з'єднаємо це з темою 16. Маршрут пошуку модуля не змінився: sys.modules, вбудовані, теки з sys.path. Змінився вміст останнього пункту — і тільки він.

Коли інтерпретатор стартує, він дивиться, чи лежить поруч із ним pyvenv.cfg. Якщо лежить — Python оголошує себе «інтерпретатором середовища»: кладе в sys.prefix шлях до .venv, а справжній корінь ховає в sys.base_prefix. Стандартна бібліотека береться за другим, пакети — за першим.

Інтерактив 4 · sys.path до і після

Той самий комп'ютер, три різні інтерпретатори. Порівняй останні рядки списку.

хто ти такий
sys.path
тек із пакетами
системні видно
Чотири перші рядки однакові скрізь — це тека запуску й стандартна бібліотека, вони беруться з базового Python. Уся різниця в хвості списку. Третій варіант створюється прапорцем --system-site-packages і потрібен рідко: коли системний пакет неможливо поставити через pip (важкі бібліотеки з бінарними частинами).
Звідки pip знає, куди ставити. Питання здається складним, а відповідь одна: pip — це звичайний модуль Python. Він дивиться на sys.prefix того інтерпретатора, яким його запустили, і складає файли туди. Тому .venv/bin/pip ставить у середовище, а /usr/bin/pip3 — у систему. І тому діагностика при будь-якій плутанині завжди одна: which python і python -c "import sys; print(sys.prefix)".

06 / Інструментpip: чотири команди щодня

pip — це менеджер пакетів Python. Він ходить у сховище PyPI (Python Package Index), завантажує звідти пакет, розпаковує його в site-packages і повторює те саме для всіх його залежностей. Команд у нього десятки, у щоденній роботі потрібні чотири.

командащо робитьдеталь, яку варто знати
pip install pandasставить пакет і всі його залежностізалежностей зазвичай більше, ніж здається: pandas тягне ще щонайменше три
pip listпоказує, що вже стоїтьу свіжому середовищі список майже порожній — це нормально
pip show pandasкартка пакетарядок Location каже, у яку саме теку він ліг; Required-by — хто на нього спирається
pip uninstall pandasприбирає пакетзалежності не прибирає — вони лишаються сиротами

Дві дрібниці, які економлять час. pip install -U pandas оновлює вже встановлений пакет до найновішої версії. А pip install pandas==2.1.4 ставить точно названу — і ось тут починається окрема історія.

07 / ЧислаВерсії й обмеження

Версія пакета зазвичай має три числа: 2.1.4. Домовленість зветься семантичне версіонування і читається так:

Тому в файлі залежностей пишуть не просто ім'я, а обмеження (specifier). Форм кілька, і різниця між ними — не стилістична:

записчитається якколи доречно
zvitnykбудь-яка версіямайже ніколи: завтра прилетить 3.0 і зламає все
zvitnyk==1.4.2рівно ця, жодної іншоїфайл, що фіксує стан для відтворення
zvitnyk>=1.4ця або новішанебезпечно саме по собі: верхньої межі немає
zvitnyk>=1.4,<2.0у межах першої гілкинайчастіший вибір для бібліотеки
zvitnyk~=1.4.2>=1.4.2, але менше за 1.5.0«приймаю лише виправлення помилок»

Остання форма — «сумісний реліз» — плутає найчастіше, бо її поведінка залежить від того, скільки чисел ти написала. ~=1.4.2 дозволяє мінятися лише третьому числу, а ~=1.4 — уже другому. Правило просте: останнє написане число може зростати, попереднє — ні. Перевір це очима:

Інтерактив 5 · Яку версію обере pip

Список усіх релізів пакета zvitnyk на PyPI. Обери запис у requirements.txt.

підходить
встановиться
Ключове правило: серед усіх версій, що підходять під обмеження, pip завжди бере найновішу. Тому обмеження — це не побажання, а стеля. Якщо стелі немає, вона з'явиться сама в найгірший момент: у день, коли автор випустить наступну велику версію.

08 / Пам'ятьrequirements.txt

Середовище зроблене, пакети стоять, усе працює. Проблема в тому, що це знання живе тільки на твоєму комп'ютері. Колега клонує проєкт — і має голий код без жодної бібліотеки. Ти сама через пів року на новому ноутбуці — у тій самій ситуації.

Розв'язок — текстовий файл зі списком того, що треба поставити. За домовленістю він зветься requirements.txt і лежить у корені проєкту. Дві команди на все:

# записати те, що стоїть у мене зараз
pip freeze > requirements.txt

# відтворити те саме в новому середовищі
pip install -r requirements.txt

Кутова дужка > — це не частина pip, а можливість оболонки: «те, що команда надрукувала б на екран, поклади у файл». Сам pip freeze просто друкує список.

Формат файлу — по рядку на пакет, і freeze завжди пише жорстке ==:

numpy==2.1.3
pandas==2.2.3
python-dateutil==2.9.0
pytz==2024.2
tzdata==2024.2

Тут п'ять рядків, хоча ставила ти два пакети. Решта — залежності залежностей, і це правильно: відтворювати треба весь стан, а не тільки те, що пам'ятаєш. Порожні рядки й рядки з # у файлі ігноруються, тож коментарі писати можна.

Навіщо фіксувати версії

А тепер головне питання теми: чому ==, а не просто ім'я пакета? Відповідь видно лише в майбутньому, тому повзунок в інтерактиві — це час. Уяви, що колега ставить залежності не сьогодні, а через кілька місяців:

Інтерактив 6 · Той самий файл через пів року

Код не змінювався. Змінилось лише те, що встиг випустити автор бібліотеки.

колезі встановиться
Найгірша властивість цього багу — відкладеність. Рядок без версії поводиться бездоганно рівно доти, доки автор не випустить 2.0. Зламається не той, хто писав файл, а той, хто прийде пізніше, — і шукатиме причину у власному коді, бо код же не змінювався.
Зворотний бік pip freeze. Він фіксує усе, зокрема випадково поставлені пакети й залежності залежностей. Через рік такий файл уже не відповідає на питання «що мій проєкт справді використовує». Тому зрілі проєкти тримають два списки: короткий, написаний руками («мені потрібні pandas і matplotlib»), і довгий згенерований, що фіксує точні версії всього. У сучасних інструментах перший — це pyproject.toml, другий — lock-файл; про них у розділі для досвідчених.

09 / Помилкаexternally-managed-environment

Тепер найпоширеніша сучасна помилка. Людина ставить Python на свіжому Ubuntu чи Fedora, пише pip install requests — і замість встановлення отримує сторінку тексту, яка починається зі слова error.

Це не поломка. Це захист, який працює саме так, як задумано.

Причина в тому, що на Linux і macOS частина операційної системи написана на Python: менеджер пакетів, службові утиліти, налаштування мережі. Усі вони імпортують бібліотеки із системного site-packages. Коли pip ставить туди свою версію якоїсь бібліотеки, він перезаписує ту, на яку розраховував менеджер пакетів, — і той перестає працювати. Полагодити це буває складніше, ніж перевстановити систему.

Тому з PEP 668 дистрибутиви кладуть поруч зі своїм Python файл-маркер EXTERNALLY-MANAGED, а pip, побачивши його, відмовляється щось ставити. Наведи мишею на частини повідомлення — і перевір праворуч усі чотири варіанти реакції:

Інтерактив 7 · Розбір повідомлення й правильна реакція

Справжній вивід pip 25 на Ubuntu. Наводь на фрагменти; перемикачем обери, що робити.

Прапорець назвали чесно. --break-system-packages — це не «дозволь мені», а «я згоден зламати системні пакети». Автори pip навмисно обрали слово, яке неприємно писати. Якщо рука тягнеться саме до нього — майже завжди правильна відповідь на рядок вище.
Чому sudo pip install гірший за все інше. Слово sudo прибирає останній запобіжник — права доступу. Наслідків три, і кожен неприємний. Перший: файли лягають у системні теки, і менеджер пакетів ОС про них не знає — при наступному оновленні виникає конфлікт, який нічим не розв'язати. Другий: якщо pip перезапише бібліотеку, від якої залежить сам менеджер пакетів, ти втратиш і спосіб це полагодити. Третій, найтихіший: під sudo код зі стороннього пакета виконується від імені адміністратора вже під час встановлення. Проста вимога, яка знімає всі три: pip ніколи не запускають через sudo.

10 / ГігієнаЧому теку не комітять

Тека .venv лежить усередині проєкту — здається логічним покласти її в репозиторій разом із кодом. Так робити не треба, і причин чотири.

Тому в корені проєкту заводять файл .gitignore — список того, що git має не помічати:

# .gitignore
.venv/
__pycache__/
*.pyc

У репозиторій потрапляє натомість requirements.txt: кілька сотень байтів замість кількох сотень мегабайтів, і при цьому вони описують той самий стан. Це і є правильний обмін.

11 / ГлибшеДля тих, хто вже писав код

Далі — сучасний шар, який новачку сьогодні не потрібен, але з яким доведеться зустрітися в будь-якому живому проєкті.

pyproject.toml замість requirements.txt

requirements.txt — формат без стандарту: це просто список рядків для pip. Сучасна заміна описана в PEP 621 і зветься pyproject.toml. Це один файл, у якому лежить усе: ім'я проєкту, версія, залежності й налаштування інструментів.

[project]
name = "afisha"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "pandas>=2.2,<3.0",
    "matplotlib>=3.9",
]

Головна відмінність не в синтаксисі, а в ролі. requirements.txt відповідає на питання «що поставити в це середовище». pyproject.toml відповідає на питання «чим є цей проєкт» — і тому його розуміє не тільки pip, а й складальники пакетів, форматувальники, лінтери. Обидва живуть поруч без конфлікту: перший фіксує точні версії, другий описує наміри.

pip install -e .

Крапка означає «пакет, що лежить у поточній теці», а прапорець -e (editable) — «не копіюй файли в site-packages, а постав туди посилання на них». Наслідок дуже приємний: твій власний пакет стає імпортованим з будь-якої теки, при цьому правки в коді підхоплюються одразу, без перевстановлення.

Це правильна відповідь на класичне «мій модуль не імпортується з підтеки». Дописувати теки руками в sys.path — милиця, яка ламається при першому ж переносі проєкту. Встановлення в режимі редагування розв'язує те саме без жодного рядка коду.

Три інструменти, про які ти почуєш

Що таке lock-файл і чим він кращий за freeze. pip freeze записує версії, але не записує вміст. Lock-файл (uv.lock, poetry.lock) додає до кожного пакета контрольну суму архіву. Різниця стає видною в неприємний день: якщо хтось підмінить уже випущену версію на PyPI, freeze цього не помітить, а перевірка хешу — помітить і зупинить встановлення.

Дрібниці, які тішать

python3 -m venv --upgrade-deps .venv одразу створює середовище зі свіжим pip — інакше перша ж команда нагадає, що всередині лежить версія з дня випуску твого Python. pip install --dry-run показує, що було б встановлено, нічого не змінюючи. pip check перевіряє, чи не суперечать одна одній версії вже встановлених пакетів. А pip download складає архіви в теку — так переносять залежності на машину без інтернету.

12 / ПідсумокЩо ми тепер уміємо

Ми навчилися давати кожному проєкту власне місце для бібліотек — і закрили цілий клас проблем, які інакше з'їдають дні:

І питання, яке з цього виростає. Ми весь час говорили про те, що щось ламається: пакет не тієї версії, файл не знайшовся, функція повернула не те. Досі така поломка означала кінець програми — traceback і зупинка. Але падати не обов'язково: Python уміє ловити помилку й вирішувати, що з нею робити. Навіть звичний ModuleNotFoundError — це насправді об'єкт, який можна перехопити й обробити. Про це — тема 18, помилки та винятки.

Далі в практиці. У practice.ipynb ти створиш справжнє віртуальне середовище прямо із зошита, зазирнеш у його pyvenv.cfg, порівняєш sys.path двох інтерпретаторів на власні очі, збереш і прочитаєш requirements.txt — і наприкінці приберешся за собою, не лишивши на диску нічого.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.