Python

Модулі, Пакети та Віртуальні Середовища

Глибоке дослідження системи модулів Python — від механізмів пошуку файлів через sys.path до кешування імпортів у sys.modules, архітектури пакетів з __init__.py та ізоляції залежностей через venv. Фундамент для будь-якого серйозного Python-проекту.

Модулі, Пакети та Віртуальні Середовища

Проблема монолітного коду: навіщо взагалі ділити програму

Уявіть, що ви розробляєте платформу для онлайн-торгівлі. Перші кілька тижнів все ідеально: один файл main.py, кілька функцій, чотири змінні. Але проходить місяць — і цей файл розростається до 2000 рядків. У ньому перемішані логіка авторизації, робота з базою даних, бізнес-правила знижок, генерація PDF-звітів та обробка платежів.

Це явище має назву — монолітний антипатерн. Він не є проблемою теоретичною: він реально гальмує розробку, ламає командну роботу та перетворює підтримку коду на катування.

Низька читабельність

Знайти функцію calculate_vat серед 2000 рядків — це пригода. IDE допомагає, але лише до певної межі. Після неї — лише Ctrl+F і терпіння.

Конфлікти у команді

Двоє розробників одночасно редагують один файл — це гарантований merge conflict у Git. Один файл = один потік змін.

Неможливість перевикористання

Ваша функція send_email з main.py потрібна у новому проекті? Доведеться або копіювати, або тягнути весь монолітний файл разом із зайвим кодом.

Неможливість тестування

Як написати unit-тест для функції, що має десятки прихованих залежностей від глобального стану у тому ж файлі? Майже ніяк.

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


Частина I: Модулі

Що таке модуль: від файлу до об'єкта

З практичної точки зору, модуль — це будь-який файл з розширенням .py. Але це спрощення. З точки зору Python-рантайму, модуль — це об'єкт типу types.ModuleType, що має власний простір імен (namespace) і зберігається у кеші sys.modules.

Коли Python виконує import calculator, він не просто «підключає файл». Він:

  1. Знаходить файл calculator.py (за алгоритмом sys.path)
  2. Компілює його у байткод (calculator.pyc)
  3. Створює новий об'єкт типу module
  4. Виконує байткод у просторі імен цього об'єкта
  5. Зберігає об'єкт у словнику sys.modules['calculator']
  6. Прив'язує ім'я calculator у поточному просторі імен

Розглянемо цей процес на практичному прикладі. Побудуємо структуру реального mini-проекту:

python main.py
$ python main.py
Сума: 15
Площа кола R=7: 153.94
PI з модуля: 3.1415926535

Простір імен модуля: захист від конфліктів

Коли ви пишете import calculator, Python не «вливає» імена add, subtract, PI у поточний простір імен. Замість цього він створює одне ім'я calculator, що вказує на об'єкт модуля. Доступ до вмісту — лише через крапку. Це і є простір імен (namespace) — ізольований контейнер для імен.

# main.py
import calculator

# Наша власна функція add — для роботи з рядками
def add(text1: str, text2: str) -> str:
    return text1 + " " + text2

# Жодного конфлікту! Два різних простори імен:
my_text = add("Привіт", "Світ")          # наша функція
calc_sum = calculator.add(2, 2)           # функція з модуля

print(my_text)    # "Привіт Світ"
print(calc_sum)   # 4

Якби Python не використовував простори імен і замість import calculator вставляв усі імена безпосередньо, виникала б катастрофа: будь-яка функція з будь-якого модуля могла б перезаписати будь-яку іншу. Саме тому import module вважається більш безпечним підходом порівняно з from module import *.

Поглиблене розуміння просторів імен (Namespaces) та LEGB-правило

Щоб зрозуміти, як працює система імпорту та як уникнути помилок, пов'язаних із видимістю змінних, потрібно розібратися, що таке простір імен (namespace) на низькому рівні.

У Python простір імен — це звичайний словник (dict), де ключами є імена змінних (ідентифікатори), а значеннями — самі об'єкти, на які ці змінні посилаються. Коли ви пишете x = 42, Python просто додає запис {"x": 42} у відповідний словник простору імен.

Чотири рівні просторів імен та LEGB-правило

Коли ви звертаєтеся до змінної, Python шукає її не скрізь, а в чітко визначеному порядку. Цей порядок описується LEGB-правилом:

Local (Локальний)

Змінні, визначені всередині поточної функції (через def або lambda). Вони створюються при виклику функції і знищуються при її завершенні.

Enclosing (Охоплюючий)

Змінні у зовнішніх функціях (використовується при вкладених функціях або замиканнях).

Global (Глобальний)

Змінні, визначені на рівні модуля (файлу). Всі функції цього модуля мають доступ до цих змінних.

Built-in (Вбудований)

Назви, які автоматично завантажуються інтерпретатором при старті. Сюди входять вбудовані функції (print(), len(), range(), dict) та виключення (ValueError, KeyError).

Схематично ієрархію пошуку імен можна зобразити у вигляді вкладених областей:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam ArrowColor #6366f1

node "Вбудований (Built-in)\n[print(), len(), ValueError]" as B #fee2e2 {
    node "Глобальний (Global)\n[змінні та функції модуля]" as G #dbeafe {
        node "Охоплюючий (Enclosing)\n[зовнішня функція]" as E #fef3c7 {
            node "Локальний (Local)\n[поточна функція]" as L #d1fae5 {
            }
        }
    }
}

note right of L
  Пошук імені починається тут (L)
  і йде назовні: L -> E -> G -> B.
  Якщо ім'я не знайдено в жодному —
  піднімається NameError.
end note
@enduml

Практичний експеримент: дослідження просторів імен

Ми можемо дослідити поточні простори імен за допомогою вбудованих функцій locals() та globals(), які повертають відповідні словники простору імен.

# namespace_demo.py
import math

global_var = "Я глобальна змінна"

def outer_function():
    enclosing_var = "Я охоплююча змінна"

    def inner_function():
        local_var = "Я локальна змінна"

        # Дивимося на локальний простір імен внутрішньої функції
        print("--- Local Namespace inside inner_function() ---")
        print(locals())  # Виведе тільки {'local_var': 'Я локальна змінна'}

        # Спробуємо знайти global_var та print (built-in)
        # Python пройде шлях: inner_local -> outer_enclosing -> global -> built-in

    inner_function()

outer_function()

# Глобальний простір імен модуля
print("\n--- Global Namespace of module ---")
# globals() містить імпортований 'math', 'global_var', 'outer_function' та службові атрибути
print(list(globals().keys())[:10])  # покажемо перші 10 ключів
python namespace_demo.py
$ python namespace_demo.py
--- Local Namespace inside inner_function() ---
{'local_var': 'Я локальна змінна'}
--- Global Namespace of module ---
['__name__', '__doc__', '__package__', 'math', 'global_var', 'outer_function']

Модуль як об'єкт та його атрибут __dict__

Оскільки кожен модуль після імпорту стає об'єктом типу module, його простір імен зберігається в спеціальному атрибуті __dict__. Це звичайний словник.

Коли ви пишете calculator.add(5, 5), Python під капотом виконує: calculator.__dict__['add'](5, 5)

Це означає, що ми можемо динамічно маніпулювати простором імен модуля навіть після його імпорту (хоча цим не варто зловживати):

import calculator

# Динамічно додаємо нове ім'я в простір імен модуля calculator
calculator.new_constant = 9.99

# Тепер це ім'я доступне звичайним шляхом!
print(calculator.new_constant)  # 9.99

Цей динамізм показує, що простір імен модулів у Python є відкритим і гнучким, на відміну від статичних просторів імен у таких мовах, як C++ або C#.

Синтаксис імпорту: вичерпний огляд

Python надає кілька синтаксичних форм для імпорту, кожна з яких має власну область застосування та компроміси.

import calculator
import os
import sys

# Доступ лише через ім'я модуля — жодних конфліктів
result = calculator.add(10, 5)
cwd = os.getcwd()
paths = sys.path

Коли використовувати: у більшості випадків. Максимальна чіткість: завжди видно, звідки прийшло ім'я.

Конвенція _private: приватні атрибути модуля

У Python немає ключових слів private або protected. Натомість існує конвенція: ім'я, що починається з одного підкреслення (_name), є сигналом «не призначене для публічного використання».

# calculator.py
PI = 3.14159          # публічний атрибут ✅
_precision = 10       # «приватний» атрибут — для внутрішнього використання

def add(a, b):        # публічна функція ✅
    return a + b

def _validate(x):     # «приватна» функція — деталь реалізації
    return isinstance(x, (int, float))

Технічно calculator._validate(5) спрацює без помилок — Python лише сигналізує, але не забороняє. Проте конвенція дотримується всією екосистемою: IDE підсвічують такий виклик, linter-и видають попередження, а рецензенти коду запитають: «Навіщо ти лізеш у внутрішності чужого модуля?»

Ця конвенція також впливає на from module import *: якщо ваш модуль не визначає __all__, зірковий імпорт пропускає всі імена з підкресленням — це одна з небагатьох корисних властивостей import *.


Система пошуку модулів: sys.path зсередини

Алгоритм пошуку import

Коли Python зустрічає import my_module, він не шукає файл одразу по всій файловій системі. Він послідовно перевіряє ієрархічний список директорій, що зберігається у sys.path — списку рядків, де кожен рядок є абсолютним шляхом до директорії.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam ArrowColor #6366f1

start

:Python зустрічає: import my_module;

:Перевірити sys.modules["my_module"];

if (знайдено у кеші?) then (Так)
    :Повернути кешований об'єкт модуля;
    stop
else (Ні)
    :Ітерація по sys.path;
    note right
      sys.path — це список:
      1. Директорія поточного скрипта
      2. Шляхи з PYTHONPATH
      3. Стандартна бібліотека
      4. site-packages (pip)
    end note
endif

:Для кожного шляху в sys.path:;

if (my_module.py існує у шляху?) then (Так)
    :Компілювати у байткод (.pyc);
    :Створити об'єкт types.ModuleType;
    :Виконати байткод у просторі імен модуля;
    :Зберегти у sys.modules["my_module"];
    :Прив'язати ім'я my_module у поточному просторі;
    stop
else (Ні)
    :Перейти до наступного шляху;
    if (Вичерпано усі шляхи?) then (Так)
        :Підняти ModuleNotFoundError;
        stop
    else (Ні)
        :Продовжити пошук;
    endif
endif
@enduml

Анатомія sys.path: що там насправді

import sys

print(f"Тип sys.path: {type(sys.path)}")  # <class 'list'>
print(f"Кількість шляхів: {len(sys.path)}")

for i, path in enumerate(sys.path):
    print(f"  [{i}] {path}")
python inspect_path.py
$ python inspect_path.py
Тип sys.path: <class 'list'>
Кількість шляхів: 6
[0] /home/user/myproject # директорія запущеного скрипта
[1] /usr/lib/python312.zip
[2] /usr/lib/python3.12 # стандартна бібліотека
[3] /usr/lib/python3.12/lib-dynload
[4] /home/user/.venv/lib/python3.12/site-packages # pip-пакети
[5] /usr/lib/python3/dist-packages

Звідки формується sys.path? Це не магія — список складається з кількох джерел у визначеному порядку пріоритету:

sys.path[0] — директорія скрипта
string
При запуску python /home/user/project/main.py Python автоматично ставить на перше місце /home/user/project/. Саме тому import calculator працює, якщо calculator.py лежить поруч з main.py. При запуску через -m або в інтерактивному режимі sys.path[0] дорівнює '' (порожній рядок = поточна директорія shell).
PYTHONPATH — змінна середовища
list[str]
Якщо встановлена змінна середовища PYTHONPATH=/my/libs:/other/libs, Python вставляє ці шляхи після sys.path[0]. Це спосіб глобально зареєструвати власні бібліотеки без встановлення через pip. Рідко потрібен у сучасних проектах з venv.
Стандартна бібліотека
list[str]
Шляхи до вбудованих модулів Python: os, sys, json, datetime тощо. Їхнє точне розташування залежить від ОС та версії Python, але вони завжди присутні у sys.path.
site-packages — pip-пакети
list[str]
Директорія, куди pip install встановлює сторонні пакети. Зазвичай знаходиться у ~/.local/lib/pythonX.Y/site-packages/ (глобально) або venv/lib/pythonX.Y/site-packages/ (у venv). Саме тут лежать ваші requests, Django, numpy.

Маніпуляції з sys.path: коли і як

sys.path — це звичайний Python-список, тому його можна змінювати в рантаймі. Це дає потужні (і небезпечні) можливості:

import sys

# Додати власний шлях перед усіма іншими (найвищий пріоритет)
sys.path.insert(0, '/path/to/my/custom/libs')

# Або в кінець (найнижчий пріоритет)
sys.path.append('/path/to/fallback/libs')

# Тепер Python шукатиме модулі і там
import my_custom_module  # знайде у /path/to/my/custom/libs
Маніпуляції з sys.path у production-коді — антипатерн. Якщо ви додаєте шлях до sys.path у своєму коді, це означає, що ваша структура проекту або система встановлення пакетів налаштована неправильно. Правильне рішення — використовувати pip install -e . для локальної розробки або коректно структурований пакет. sys.path.append(...) допустимий лише у тимчасових скриптах та скриптах налагодження.

Альтернатива — файл .pth у директорії site-packages: Python автоматично читає всі .pth-файли при запуску та додає зазначені в них шляхи до sys.path. Команда pip install -e . (editable install) використовує саме цей механізм.

Як влаштований імпорт під капотом: Finder та Loader

Багато розробників думають, що import — це просто пошук файлу на диску. Насправді Python використовує складну, але дуже гнучку двохетапну систему імпорту, що базується на пошуковцях (Finders) та завантажувачах (Loaders). Ця система повністю документована у PEP 302.

Коли ви викликаєте import my_module, виконуються такі кроки:

  1. Пошук модуля (Find Phase): Python проходить по списку пошуковців, що зберігаються в sys.meta_path. Типово там містяться:
    • BuiltinImporter (для вбудованих модулів, наприклад, sys).
    • FrozenImporter (для "заморожених" модулів, написаних на C і скомпільованих у двійковий файл інтерпретатора).
    • PathFinder (шукає файли на диску за шляхами із sys.path).

    Кожен пошуковець намагається знайти модуль. Якщо знаходить, він повертає спеціальний об'єкт — специфікацію модуля (ModuleSpec), яка містить метадані: ім'я модуля, посилання на завантажувач та шлях до файлу.
  2. Завантаження модуля (Load Phase): Отримавши ModuleSpec, Python викликає відповідний завантажувач (Loader). Завантажувач:
    • Створює порожній об'єкт модуля.
    • Записує його в sys.modules.
    • Виконує код файлу модуля в контексті цього об'єкта.

Цей механізм дозволяє розробникам писати власні імпортери (import hooks). Наприклад, можна написати Finder/Loader, який імпортує модулі безпосередньо з ZIP-архівів, бази даних або навіть завантажує їх через HTTP з віддаленого сервера!

Що таке .pth-файли

Файли з розширенням .pth (path configuration files) — це дуже простий і зручний спосіб додавання шляхів до sys.path без ручного редагування коду або налаштування змінної PYTHONPATH.

Якщо покласти файл my_paths.pth у директорію site-packages вашого віртуального середовища, Python під час ініціалізації прочитає його рядки і додасть усі вказані там шляхи до sys.path.

Кожен рядок у .pth-файлі має містити один абсолютний або відносний шлях. Якщо рядок починається з import , Python навіть виконає цей код (це використовується деякими складними бібліотеками для автоналаштування).

Саме цей механізм використовується при встановленні локального пакета в "режимі редагування": pip install -e . Замість того, щоб копіювати файли вашого проекту в site-packages, pip просто створює там .pth-файл, який містить шлях до вашої робочої директорії. Це дозволяє вам редагувати код, і зміни будуть миттєво видимі без повторного встановлення пакета.


Кешування імпортів: sys.modules та Singleton-поведінка

Чому модуль завантажується лише один раз

Python є мовою, де один і той самий модуль (os, json, datetime) може бути імпортований у десятках різних файлів одного проекту. Якби кожен import os перечитував файл з диска та виконував байткод заново — продуктивність була б катастрофічною.

Рішення — кеш модулів: словник sys.modules, що зберігає всі вже завантажені модулі.

# main.py — демонстрація кешування
import sys

print("=== Перший імпорт ===")
import calculator  # <— тут: файл читається, байткод виконується

print("\n=== Другий імпорт ===")
import calculator  # <— тут: Python бачить 'calculator' у sys.modules → пропускає

print("\n=== Третій імпорт ===")
import calculator  # <— те саме: кеш-хіт, нульова вартість

print(f"\n'calculator' у кеші? {'calculator' in sys.modules}")

# Отримати об'єкт модуля з кешу напряму
cached = sys.modules['calculator']
print(f"Той самий об'єкт? {calculator is cached}")   # True
print(f"Виклик через кеш: {cached.add(1, 2)}")        # 3
Кешування: модуль завантажується лише раз
$ python main.py
=== Перший імпорт ===
Модуль calculator завантажено! # виводиться лише раз
=== Другий імпорт ===
# нічого — кеш-хіт
=== Третій імпорт ===
# нічого — кеш-хіт
'calculator' у кеші? True
Той самий об'єкт? True
Виклик через кеш: 3

Експеримент: ручне керування кешем у sys.modules

Щоб краще зрозуміти роль sys.modules, ми можемо втрутитися в його роботу безпосередньо. Якщо ми видалимо модуль із цього словника, Python "забуде", що він його колись імпортував, і при наступному import виконає код заново (так, ніби це перший імпорт).

# sys_modules_experiment.py
import sys

print("--- Крок 1: Перший імпорт ---")
import calculator  # Виведе повідомлення про завантаження

print("\n--- Крок 2: Другий імпорт (кешований) ---")
import calculator  # Нічого не виведе, оскільки calculator вже у sys.modules

# Перевіряємо наявність у кеші
print(f"\ncalculator у sys.modules? {'calculator' in sys.modules}")

print("\n--- Крок 3: Видалення з sys.modules та повторний імпорт ---")
# Ручне видалення запису з кешу
del sys.modules['calculator']

print(f"calculator у sys.modules після видалення? {'calculator' in sys.modules}")

import calculator  # Python знову завантажує та виконує код модуля!
python sys_modules_experiment.py
$ python sys_modules_experiment.py
--- Крок 1: Перший імпорт ---
Модуль calculator завантажено!
--- Крок 2: Другий імпорт (кешований) ---
# Кеш-хіт: повторного виводу немає
calculator у sys.modules? True
--- Крок 3: Видалення з sys.modules та повторний імпорт ---
calculator у sys.modules після видалення? False
Модуль calculator завантажено! # Код виконався знову!
Хоча цей трюк демонструє роботу sys.modules, ніколи не видаляйте модулі з sys.modules вручну в реальних проектах. Це може зламати зв'язки між модулями та спричинити непередбачувані побічні ефекти (наприклад, якщо інші частини коду вже мають посилання на старий об'єкт модуля).

sys.modules як Singleton-реєстр: спільний стан

Той факт, що Python повертає один і той самий об'єкт модуля всім імпортерам, породжує важливий архітектурний наслідок: модуль з глобальними змінними веде себе як Singleton.

# state.py — модуль зі спільним станом
print("Ініціалізація state.py")
request_count = 0
active_connections = []

# module_a.py — реєструє запити
import state

def handle_request(url: str) -> None:
    state.request_count += 1
    state.active_connections.append(url)
    print(f"[A] Запит #{state.request_count} до {url}")

# module_b.py — читає статистику
import state

def get_stats() -> dict:
    return {
        "total": state.request_count,
        "active": len(state.active_connections)
    }

# main.py
import module_a
import module_b

module_a.handle_request("/api/users")
module_a.handle_request("/api/products")

stats = module_b.get_stats()
print(f"Статистика: {stats}")
# {'total': 2, 'active': 2}
# module_b бачить зміни, зроблені module_a!

Обидва модулі отримали одне й те саме посилання на об'єкт state з sys.modules. Зміни від module_a одразу видимі у module_b. Це потужний механізм для глобальної конфігурації та стану, але він вимагає свідомого підходу — ненавмисні зміни глобальних змінних модуля є поширеним джерелом важко відтворюваних багів.

Перезавантаження модуля: importlib.reload

У нормальному виробничому коді кеш sys.modules є бажаною поведінкою. Але при інтерактивній розробці (Jupyter Notebook, REPL) виникає ситуація: ви змінили файл модуля, але Python все одно використовує стару закешовану версію.

import importlib
import calculator

# Поточна версія
print(calculator.add(1, 2))   # 3

# ... ви зміни код calculator.py: add тепер повертає a + b + 100 ...

# Звичайний import нічого не зробить:
import calculator
print(calculator.add(1, 2))   # все одно 3!

# Примусово перезавантажити:
importlib.reload(calculator)
print(calculator.add(1, 2))   # тепер 103 ✅

importlib.reload не видаляє об'єкт модуля з sys.modules — він повторно виконує код файлу в тому ж самому об'єкті модуля, оновлюючи його атрибути. Це має важливе обмеження: якщо ви зробили from calculator import add (імпорт із прив'язкою до локального імені), reload(calculator) оновить модуль, але ваша локальна змінна add все одно вказуватиме на стару функцію.

importlib.reload — виключно інструмент для розробки. У production-коді перезавантаження модулів у рантаймі є джерелом важко передбачуваних помилок, пов'язаних із невідповідністю об'єктів, що були створені до і після reload. Не використовуйте його у фінальному коді.

Подвійне призначення файлу: if __name__ == "__main__"

Як Python визначає «хто головний»

Кожен модуль Python несе атрибут __name__. Його значення залежить від того, в якому контексті виконується файл:

  • Якщо файл запускається напряму (python calculator.py): __name__ == "__main__"
  • Якщо файл імпортується в інший модуль: __name__ == "calculator" (ім'я без .py)

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

# calculator.py
PI = 3.14159

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def _run_tests():
    """Вбудовані smoke-тести."""
    assert add(2, 3) == 5, "add(2, 3) має дорівнювати 5"
    assert subtract(10, 3) == 7, "subtract(10, 3) має дорівнювати 7"
    print("✅ Всі smoke-тести пройдено!")

# Цей блок виконується ЛИШЕ при запуску: python calculator.py
# При import calculator — він повністю ігнорується
if __name__ == "__main__":
    print(f"Запуск calculator.py як головного скрипта.")
    print(f"Поточне __name__: {__name__}")
    _run_tests()
Два контексти одного файлу
$ python calculator.py # запуск напряму
Запуск calculator.py як головного скрипта.
Поточне __name__: __main__
✅ Всі smoke-тести пройдено!
$ python main.py # main.py робить: import calculator
# Нічого з if-блоку не виконується!
calculator.__name__ == 'calculator' # не __main__

Патерн main(): структурований точок входу

Поєднання if __name__ == "__main__" зі спеціальною функцією main() є галузевим стандартом для скриптів, що мають власну логіку виконання:

# data_processor.py
import sys
import json

def load_data(filepath: str) -> dict:
    """Завантажує JSON з файлу."""
    with open(filepath) as f:
        return json.load(f)

def process(data: dict) -> list:
    """Бізнес-логіка обробки даних."""
    return [item for item in data.get("items", []) if item.get("active")]

def save_results(results: list, output_path: str) -> None:
    """Зберігає результати."""
    with open(output_path, "w") as f:
        json.dump(results, f, indent=2)

def main() -> int:
    """
    Точка входу програми.
    Повертає код завершення: 0 = успіх, 1 = помилка.
    """
    if len(sys.argv) != 3:
        print(f"Використання: python {sys.argv[0]} <input.json> <output.json>")
        return 1

    input_path = sys.argv[1]
    output_path = sys.argv[2]

    try:
        data = load_data(input_path)
        results = process(data)
        save_results(results, output_path)
        print(f"✅ Оброблено {len(results)} записів → {output_path}")
        return 0
    except FileNotFoundError as e:
        print(f"❌ Файл не знайдено: {e}")
        return 1
    except json.JSONDecodeError as e:
        print(f"❌ Невалідний JSON: {e}")
        return 1


if __name__ == "__main__":
    sys.exit(main())  # sys.exit передає код завершення в оболонку

Цей підхід дає три переваги: main() є звичайною функцією, яку можна легко тестувати; return 1 / return 0 дозволяє коректно сигналізувати про помилки в bash-скриптах; модуль залишається повністю придатним для import.

python -m: запуск модуля як скрипта

Окрім python script.py, Python підтримує синтаксис python -m module_name. Різниця тонка, але важлива:

Командаsys.path[0]Для чого
python path/to/script.pyдиректорія скриптапрості скрипти
python -m package.moduleкоренева директорія проектумодулі всередині пакетів
python -m venv .venvвбудовані інструменти

Прапорець -m каже Python знайти модуль за sys.path і запустити його з __name__ = "__main__". Це критично для пакетів, де відносні імпорти (from .utils import helper) не працюють при прямому python package/module.py, але коректно розв'язуються при python -m package.module.


Циклічні імпорти: архітектурний антипатерн

Як виникає циклічний імпорт

Циклічний імпорт виникає, коли модуль A імпортує модуль B, а модуль B імпортує модуль A (прямо чи через ланцюг інших модулів). Python обробляє цей сценарій, але результат часто несподіваний.

# a.py
print("Завантаження a.py...")
import b  # Python призупиняє a.py і завантажує b.py

def func_a():
    b.func_b()
    print("func_a виконана")

print("a.py завантажено.")

# b.py
print("Завантаження b.py...")
import a  # Python бачить: a вже є у sys.modules (частково!)

def func_b():
    print("func_b виконана")

print("b.py завантажено.")

# main.py
import a
a.func_a()  # AttributeError: partially initialized module 'b'...

Що відбувається покроково:

Python починає виконувати a.py

Виводить Завантаження a.py.... Бачить import b — призупиняється.

Python починає виконувати b.py

Виводить Завантаження b.py.... Бачить import a.

Python виявляє a у sys.modules

Але a ще не повністю завантажений — він призупинений на рядку import b. Python не падає в рекурсію, а повертає частковий об'єкт a. На момент повернення у a.__dict__ ще немає func_a (оголошення def func_a() ще не виконане!).

b.py успішно завантажується

Визначає func_b, виводить b.py завантажено.

Виконання повертається до a.py

a.py продовжує: визначає func_a, виводить a.py завантажено.

Виклик a.func_a()b.func_b()

Якщо b.py намагається використати щось з a на рівні модуля (не всередині функції), воно буде недоступне.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam ArrowColor #6366f1

participant "Python" as PY #f3f4f6
participant "a.py\n(частково завантажений)" as A #fef3c7
participant "b.py" as B #d1fae5
participant "sys.modules" as SM #e0e7ff

PY -> A : Виконати a.py
A -> SM : Зареєструвати a (частково)
note right of SM
  sys.modules['a'] = <module a>
  (але func_a ще не визначена!)
end note

A -> B : import b → виконати b.py
B -> SM : Перевірити: 'a' у sys.modules?
SM --> B : Так! Повернути частковий об'єкт a
note right of B
  b.py отримує модуль a,
  але у ньому немає func_a!
end note

B -> SM : Зареєструвати b (повністю)
B --> A : import b завершено
A -> SM : Оновити a (додати func_a)

note bottom of SM
  Тепер a повний.
  Але якщо b.py використовував a.func_a
  на рівні модуля — вже пізно!
end note
@enduml

Правильні рішення

Рішення 1 (найкраще): Рефакторинг — виділення спільного коду у третій модуль.

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

❌ Погана архітектура:          ✅ Правильна архітектура:
  a.py ←→ b.py                   shared.py (незалежний)
                                  a.py → shared.py
                                  b.py → shared.py, a.py

Рішення 2 (тимчасове): Локальний імпорт всередині функції.

# a.py
def func_a():
    import b  # ← імпорт всередині функції, відкладений до виклику
    b.func_b()
    print("func_a виконана")

Коли func_a() буде викликана, обидва модулі вже будуть повністю завантажені, тому b.func_b буде доступна. Але це «милиця» — вона ховає архітектурну проблему і уповільнює виклик функції (перевірка sys.modules при кожному виклику).


Частина II: Пакети

Від модулів до ієрархії: навіщо потрібні пакети

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

❌ Хаос без пакетів:          ✅ Структура з пакетами:
myproject/                    myproject/
  main.py                       main.py
  users.py                      users/
  user_auth.py                    __init__.py
  user_profile.py                 auth.py
  products.py                     profile.py
  product_catalog.py            products/
  product_pricing.py              __init__.py
  orders.py                       catalog.py
  order_processing.py             pricing.py
  order_notifications.py        orders/
  database.py                     __init__.py
  api_client.py                   processing.py
  utils.py                        notifications.py
                                database.py
                                utils.py

Пакет — це директорія, що містить спеціальний файл __init__.py. Наявність цього файлу є сигналом для Python: «ця директорія — не просто папка, це модульна одиниця».

Анатомія пакета: реальний проект e-shop

Побудуємо повноцінний пакет для інтернет-магазину та дослідимо кожен його елемент:

Роль __init__.py: ворота пакета

Файл __init__.py виконує три ключові функції.

Функція 1: Позначення директорії як пакета. Без __init__.py Python 3 усе одно може знайти пакет (завдяки механізму «namespace packages»), але поведінка буде непередбачуваною. Явний __init__.py є гарантією.

Функція 2: Ініціалізаційний код. Все, що у __init__.py, виконується при першому import shop або import shop.products — але лише один раз (кеш sys.modules). Тут доречно: підключення до БД, завантаження конфігурації, ініціалізація глобального стану.

Функція 3: «Фасад» публічного API. Це найважливіша архітектурна роль. Ре-експортуючи функції у __init__.py, ви:

  • Спрощуєте зовнішній API: shop.list_products() замість shop.products.list_products()
  • Дозволяєте змінювати внутрішню структуру пакета без зламу зовнішнього коду
# До рефакторингу: shop/products.py
# Після рефакторингу: shop/catalog/products.py

# Але __init__.py залишається незмінним:
from .catalog.products import list_products  # ← просто змінили шлях
# Зовнішній код 'shop.list_products()' не сломається!

Абсолютний та відносний імпорт

Абсолютний імпорт: повний шлях від кореня

Абсолютний імпорт вказує повний шлях від кореня проекту (або від директорії у sys.path):

# Де б не знаходився цей файл у структурі проекту:
import shop.products                        # весь модуль
from shop.products import list_products     # конкретна функція
from shop.orders.processing import create_order  # з підпакета

Абсолютні імпорти є однозначними: завжди зрозуміло, звідки береться модуль. PEP 8 рекомендує абсолютний імпорт як основний для міжпакетних залежностей.

Відносний імпорт: шлях відносно поточного модуля

Відносний імпорт використовує крапкову нотацію для вказівки шляху відносно поточного модуля:

# Всередині shop/orders/processing.py:
from . import notifications       # . = поточний пакет (shop/orders/)
from .notifications import send   # конкретна функція з сусіднього модуля
from .. import products           # .. = батьківський пакет (shop/)
from ..customers import get_customer  # конкретна функція з батька

Відносні імпорти мають суттєве обмеження: вони не працюють у файлі, що виконується як головний скрипт (__name__ == "__main__"). Тому не використовуйте відносні імпорти у main.py або в будь-якому файлі, що планується запускати напряму.

Коли використовувати відносний vs абсолютний імпорт:
  • Абсолютний — для імпорту між різними пакетами: from shop.products import ...
  • Відносний — для зв'язків усередині одного пакета: from .utils import helper — гарантує, що пакет використовує свій власний utils, а не однойменний модуль з іншого місця у sys.path

__all__: контракт публічного API

Змінна __all__ у __init__.py або у будь-якому модулі — це список рядків, що визначає, які імена є публічним API цього модуля/пакета:

# shop/products.py

__all__ = ["list_products", "get_product", "calculate_price_with_vat"]
# _products_db та будь-які внутрішні допоміжні функції
# не включені в __all__ і не будуть імпортовані через 'from shop.products import *'

def list_products(active_only=True): ...
def get_product(product_id): ...
def calculate_price_with_vat(product_id, vat_rate=0.20): ...
def _validate_product(data): ...  # внутрішня, не у __all__

_products_db = [...]              # приватні дані, не у __all__

__all__ виконує дві функції:

  1. Документація: чітко сигналізує, які об'єкти є стабільним публічним API
  2. Контроль import *: тільки імена з __all__ потрапляють у простір імен при from module import *

Частина III: Віртуальні Середовища

Проблема глобального Python: «пекло залежностей»

Уявіть типову ситуацію: у вас на комп'ютері два Python-проекти.

  • Проект А (2019 рік): використовує Django 2.2 та requests 2.20
  • Проект Б (2024 рік): потребує Django 4.2 та requests 2.31

Якщо встановити обидва проекти у глобальний Python (без ізоляції), виникає конфлікт: pip install Django==4.2 замінить Django==2.2 — і Проект А зламається. Встановити обидві версії одночасно у глобальний Python неможливо.

Це явище називається «пекло залежностей» (dependency hell).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam ArrowColor #e11d48

package "Глобальний Python (❌ БЕЗ venv)" {
    object "site-packages" as SP #fee2e2 {
        Django = ???  (2.2 або 4.2?)
        requests = ???
    }
    note right of SP
      Неможливо встановити
      дві версії одночасно!
    end note
}

object "Проект А\n(Django 2.2)" as PA #fef3c7
object "Проект Б\n(Django 4.2)" as PB #fef3c7

PA --> SP : потребує Django 2.2
PB --> SP : потребує Django 4.2

note bottom
  pip install django==4.2 знищить django==2.2
  Проект А зламається!
end note
@enduml
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam ArrowColor #6366f1

package "venv_project_a/" #d1fae5 {
    object "site-packages A" as SPA #d1fae5 {
        Django = 2.2.28
        requests = 2.20.0
    }
}

package "venv_project_b/" #dbeafe {
    object "site-packages B" as SPB #dbeafe {
        Django = 4.2.7
        requests = 2.31.0
    }
}

object "Проект А" as PA #fef3c7
object "Проект Б" as PB #fef3c7

PA --> SPA : ізольоване середовище ✅
PB --> SPB : ізольоване середовище ✅

note bottom
  Обидва проекти незалежні.
  Жодних конфліктів!
end note
@enduml

Що таке venv: ізольований Python від стандартної бібліотеки

venv — це стандартний модуль Python (доступний без встановлення з Python 3.3+), що створює ізольоване середовище виконання. Технічно це директорія з окремою копією інтерпретатора Python та власною директорією site-packages.

При активації venv оболонка перенаправляє команди python та pip на копії у середовищі. Будь-яка встановлена бібліотека потрапляє у site-packages цього середовища — повністю ізольовано від глобального Python та інших середовищ.

Як venv працює під капотом

Багато хто вважає, що віртуальне середовище — це повна копія інтерпретатора Python. Насправді це дуже легка структура, яка працює завдяки кільком системним конфігураціям та логіці пошуку шляхів самого Python.

Ключовим елементом будь-якого venv є файл pyvenv.cfg, який лежить у корені папки середовища. Його вміст виглядає приблизно так:

home = /usr/bin
include-system-site-packages = false
version = 3.12.2
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/user/myproject/.venv

Коли ви запускаєте файл .venv/bin/python (або .venv/Scripts/python.exe на Windows), відбувається такий алгоритм:

  1. Пошук конфігурації: Інтерпретатор шукає файл pyvenv.cfg у своїй директорії або директорії на рівень вище.
  2. Встановлення префіксів:
    • Якщо файл pyvenv.cfg знайдено, Python розуміє, що він запущений всередині віртуального середовища.
    • Змінна sys.base_prefix встановлюється на шлях із параметра home (це глобальна установка Python, де лежить реальний виконуваний файл і стандартна бібліотека).
    • Змінна sys.prefix встановлюється на директорію, де лежить pyvenv.cfg (тобто на ваше віртуальне середовище).
  3. Формування sys.path: При формуванні шляхів пошуку модулів Python використовує значення sys.prefix. Завдяки цьому він підключає site-packages із папки .venv/lib/python3.X/site-packages/ замість глобального site-packages.

Скрипт активації (source .venv/bin/activate) насправді є дуже простим оболонковим (shell) скриптом, який робить лише дві речі:

  1. Додає шлях до .venv/bin/ на самий початок змінної середовища вашої системи PATH. Завдяки цьому, коли ви вводите python або pip, викликаються саме файли з вашого середовища, а не глобальні.
  2. Тимчасово змінює промпт вашого терміналу, додаючи (.venv) .
Активація через source — це зручність для терміналу. Програми та IDE можуть працювати з venv без будь-якої активації — їм просто достатньо запускати інтерпретатор безпосередньо за шляхом .venv/bin/python.

Повний цикл роботи з venv

Крок 1: Створення середовища

# python -m venv <назва_директорії>
# Конвенція: називати середовище 'venv' або '.venv'

python -m venv venv        # звичайне середовище
python -m venv .venv       # приховане (починається з крапки)
python3.12 -m venv .venv   # явна версія Python

Після виконання команди Python створює таку структуру:

.venv/
├── bin/                    # (Linux/macOS)
│   ├── python              # символічне посилання на інтерпретатор
│   ├── python3
│   ├── pip
│   └── activate            # скрипт активації!
├── lib/
│   └── python3.12/
│       └── site-packages/  # сюди pip встановлює пакети
└── pyvenv.cfg              # конфігурація середовища

Крок 2: Активація середовища

Активація — це зміна змінних середовища оболонки (PATH, VIRTUAL_ENV) так, щоб команди python та pip вели до venv.

source .venv/bin/activate

# Підтвердження активації: у промпті з'явиться назва середовища
# (.venv) user@host:~/myproject$

which python  # /home/user/myproject/.venv/bin/python
which pip     # /home/user/myproject/.venv/bin/pip

Крок 3: Встановлення пакетів

Після активації pip install встановлює пакети ізольовано у .venv/lib/python3.X/site-packages/:

# Встановлення з PyPI
pip install requests                   # остання версія
pip install requests==2.31.0           # конкретна версія
pip install "requests>=2.28,<3.0"      # діапазон версій
pip install django djangorestframework  # кілька одразу

# Список встановлених пакетів
pip list

# Детальна інформація про пакет
pip show requests
Процес встановлення пакета у venv
(.venv) $ pip install requests
Collecting requests
Downloading requests-2.31.0-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 1.2 MB/s eta 0:00:00
Collecting charset-normalizer<4,>=2 (from requests)
Collecting idna<4,>=2.5 (from requests)
Collecting urllib3<3,>=1.21.1 (from requests)
Collecting certifi>=2017.4.17 (from requests)
Installing collected packages: urllib3, idna, certifi, charset-normalizer, requests
Successfully installed certifi-2024.2.2 charset-normalizer-3.3.2 idna-3.7 requests-2.31.0 urllib3-2.2.1

Крок 4: Деактивація

deactivate

# Промпт повернувся до нормального вигляду:
# user@host:~/myproject$

which python  # /usr/bin/python3  ← глобальний Python

requirements.txt: заморожування залежностей

Файл requirements.txt — це текстовий маніфест точних версій усіх пакетів, потрібних вашому проекту. Він є стандартом відтворюваності Python-проектів.

Генерація requirements.txt з активного середовища:

# pip freeze виводить всі встановлені пакети з точними версіями
pip freeze > requirements.txt

cat requirements.txt
requirements.txt: зафіксовані залежності
(.venv) $ pip freeze > requirements.txt
$ cat requirements.txt
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.7
requests==2.31.0
urllib3==2.2.1

Відтворення середовища з requirements.txt:

# На іншій машині або у CI/CD:
python -m venv .venv
source .venv/bin/activate

pip install -r requirements.txt
# pip встановить точно ті ж версії, що зафіксовані у файлі
Сучасна альтернатива: Сьогодні у нових проектах часто використовують pyproject.toml (стандарт PEP 517/518) замість requirements.txt. Менеджери пакетів poetry, uv, pdm — надають більш потужне управління залежностями. Але requirements.txt + venv залишаються базовим стандартом, який потрібно знати.

Частина IV: Сучасні менеджери пакетів

Стандартний тандем venv + pip + requirements.txt надійний і повсюдно підтримуваний. Але у виробничих проектах він має відомі недоліки:

  • pip freeze включає транзитивні залежності (ті, що потрібні вашим залежностям), а не лише прямі — файл стає важким до читання і крихким при оновленні.
  • Немає детермінованого lockfile зі сумами хешів для кожної платформи окремо.
  • Немає вбудованого управління версіями Python або запуску команд у ізольованому середовищі.
  • pip не вирішує конфлікти залежностей наперед — він просто встановлює і повідомляє про помилку вже після.

Два найпопулярніші інструменти, що вирішують ці проблеми: uv (наступне покоління, швидкість у 10–100 разів вища за pip) та Poetry (зрілий, широко розповсюджений у промисловому середовищі).


uv — ультрашвидкий менеджер нового покоління

uv — це інструмент від компанії Astral (автори лінтера ruff), написаний на Rust. Він замінює одразу кілька інструментів: pip, pip-tools, virtualenv, pyenv та частково poetry — з єдиного бінарного файлу.

10–100x швидше за pip

uv використовує паралельне завантаження пакетів і оптимізований резолвер залежностей. Встановлення Django з усіма залежностями займає менше секунди проти 10–30 секунд у pip.

Єдиний інструмент

Управляє версіями Python (uv python install 3.12), середовищами (uv venv), залежностями (uv add/remove) та інструментами CLI (uvx ruff). Один бінарний файл замість п'яти.

Детермінований lockfile

uv.lock фіксує точні версії та хеші для всіх платформ. Синхронізація через uv sync гарантує ідентичне середовище на будь-якій машині.

pyproject.toml (PEP 621)

Використовує стандарт pyproject.toml як єдину точку конфігурації замість requirements.txt + setup.py + setup.cfg.

Встановлення uv

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# Також через pip (якщо вже є Python):
pip install uv

# Перевірка встановлення
uv --version  # uv 0.5.x (або новіше)

Створення нового проекту з нуля

# Ініціалізація проекту — створює pyproject.toml і базову структуру
uv init my-web-api
cd my-web-api

# Або ініціалізація у поточній директорії
mkdir my-project && cd my-project
uv init
uv init — структура нового проекту
$ uv init my-web-api
Initialized project `my-web-api` at `/Users/user/my-web-api`
$ tree my-web-api/
my-web-api/
├── .python-version ← фіксована версія Python
├── pyproject.toml ← конфігурація проекту
├── README.md
└── hello.py ← точка входу

Після uv init проект вже має pyproject.toml:

# pyproject.toml (генерується автоматично)
[project]
name = "my-web-api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

Управління залежностями

# Додавання залежності — оновлює pyproject.toml і uv.lock автоматично
uv add fastapi
uv add "sqlalchemy>=2.0"
uv add "pydantic>=2.5,<3.0"

# Кілька залежностей одразу
uv add httpx aiohttp redis

# Залежності лише для розробки (не потрапляють у production)
uv add --dev pytest pytest-asyncio ruff mypy
uv add --dev black isort

# Видалення залежності
uv remove redis

# Синхронізація середовища з lockfile (аналог pip install -r requirements.txt)
uv sync

# Синхронізація без dev-залежностей (для production)
uv sync --no-dev
uv add — швидке додавання залежності
$ uv add fastapi uvicorn
Resolved 15 packages in 287ms
Installed 15 packages in 423ms
+ annotated-types==0.7.0
+ anyio==4.7.0
+ fastapi==0.115.6
+ h11==0.14.0
+ httpcore==1.0.7
+ idna==3.10
+ pydantic==2.10.3
+ pydantic-core==2.27.1
+ sniffio==1.3.1
+ starlette==0.41.3
+ typing-extensions==4.12.2
+ uvicorn==0.32.1

Після виконання uv add pyproject.toml оновлюється автоматично:

[project]
name = "my-web-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.115.6",
    "uvicorn>=0.32.1",
]

[dependency-groups]
dev = [
    "pytest>=8.3.4",
    "ruff>=0.8.4",
    "mypy>=1.13.0",
]

Запуск коду та команд

uv run запускає скрипт або команду у керованому середовищі проекту — без потреби вручну активувати середовище:

# Запуск скрипта
uv run python main.py

# Запуск застосунку
uv run uvicorn main:app --reload

# Запуск тестів
uv run pytest

# Запуск лінтера
uv run ruff check .

# uv run також може виконувати скрипти без встановлення:
# навіть якщо pytest не у залежностях — uv встановить тимчасово
uv run --with pytest pytest tests/
uv run автоматично синхронізує середовище перед запуском, якщо uv.lock та встановлені пакети не відповідають одне одному. Це робить його ідеальним для CI/CD: не потрібні окремі кроки venv create та pip install.

Управління версіями Python

Одна з найпотужніших функцій uv — вбудоване управління версіями Python (аналог pyenv):

# Встановлення конкретної версії Python
uv python install 3.12
uv python install 3.11 3.13  # кілька версій одразу

# Перегляд доступних та встановлених версій
uv python list

# Прив'язка версії Python до проекту
uv python pin 3.12  # записує у .python-version

# Використання конкретної версії при ініціалізації проекту
uv init --python 3.11 legacy-service
uv python list — огляд версій
$ uv python list
cpython-3.13.1-macos-aarch64-none /Users/user/.local/share/uv/python/cpython-3.13.1...
cpython-3.12.8-macos-aarch64-none /Users/user/.local/share/uv/python/cpython-3.12.8... ← активна
cpython-3.11.11-macos-aarch64-none /Users/user/.local/share/uv/python/cpython-3.11.11...
cpython-3.10.16-macos-aarch64-none /Users/user/.local/share/uv/python/cpython-3.10.16...

Запуск CLI-інструментів через uvx

uvx — це аналог npx з Node.js: запускає CLI-інструменти без встановлення у проект, у тимчасовому ізольованому середовищі:

# Запуск без встановлення (uvx = uv tool run)
uvx ruff check .          # запускає ruff для перевірки поточної директорії
uvx black myfile.py        # форматує файл
uvx httpie GET httpbin.org/get  # HTTP-клієнт для тестування API

# Встановлення інструментів глобально (available everywhere)
uv tool install ruff
uv tool install black
uv tool list               # список встановлених інструментів
uvx особливо зручний у CI/CD та скриптах, де не хочеться «забруднювати» залежності проекту інструментами розробки. uvx ruff check . замість pip install ruff && ruff check ..

uv як замінник pip у існуючих проектах

Якщо у вас вже є проект з requirements.txt, uv можна підключити без переходу на pyproject.toml:

# uv як прямий замінник pip (набагато швидший)
uv pip install requests
uv pip install -r requirements.txt
uv pip freeze > requirements.txt

# Створення venv через uv
uv venv .venv
source .venv/bin/activate

# Компіляція requirements (аналог pip-tools)
uv pip compile requirements.in -o requirements.txt
Порівняння швидкості: pip vs uv
$ time pip install django djangorestframework psycopg2-binary
Successfully installed django-5.1.4 djangorestframework-3.15.2 ...
real 0m28.3s
$ time uv pip install django djangorestframework psycopg2-binary
Resolved 8 packages in 143ms
Installed 8 packages in 614ms
real 0m0.8s ← у 35 разів швидше!

Довідник команд uv

uv init
Ініціалізація проекту
Створює новий проект у поточній директорії або у вказаній папці. Генерує pyproject.toml, README.md та .python-version.
uv init                        # у поточній папці
uv init my-project             # у новій папці my-project/
uv init --python 3.11 api      # з фіксованою версією Python
uv add / uv remove
Управління залежностями
Додає або видаляє залежності. Автоматично оновлює pyproject.toml та uv.lock. Підтримує версійні обмеження, extras та групи.
uv add fastapi                       # остання версія
uv add "sqlalchemy>=2.0,<3"         # з обмеженнями версій
uv add uvicorn[standard]             # з extras
uv add --dev pytest ruff mypy        # dev-залежності
uv remove redis                      # видалення
uv sync
Синхронізація середовища
Встановлює/оновлює пакети у середовищі відповідно до uv.lock. Аналог pip install -r requirements.txt, але точний і детермінований.
uv sync                  # встановити всі залежності
uv sync --no-dev         # тільки production (без dev-групи)
uv sync --frozen         # не оновлювати lockfile, лише встановити
uv run
Запуск команд у середовищі
Виконує скрипт або команду у середовищі проекту без ручної активації venv. Перед запуском автоматично синхронізує середовище.
uv run python main.py              # запуск скрипта
uv run uvicorn app:app --reload    # запуск застосунку
uv run pytest                      # тести
uv run --with httpx python -c "import httpx; print(httpx.__version__)"
uv python
Управління версіями Python
Встановлює, перелічує та прив'язує версії Python до проекту. Замінює pyenv для більшості сценаріїв.
uv python install 3.12          # завантажити та встановити
uv python install 3.11 3.13     # кілька версій одразу
uv python list                  # переглянути всі доступні
uv python pin 3.12              # зафіксувати у .python-version
uv pip
pip-сумісний режим
Підмножина команд pip з тим самим синтаксисом, але у рази швидша. Для інтеграції з існуючими проектами без переходу на pyproject.toml.
uv pip install requests             # встановлення пакета
uv pip install -r requirements.txt  # з файлу
uv pip freeze > requirements.txt    # збереження списку
uv pip list                         # перелік встановлених
uv venv .venv                       # створення середовища
uvx / uv tool
Глобальні CLI-інструменти
uvx запускає CLI-інструменти без встановлення у проект (у тимчасовому ізольованому середовищі). uv tool install — встановлює глобально.
uvx ruff check .               # запуск без встановлення
uvx black myfile.py
uvx --from httpie http GET httpbin.org/get
uv tool install ruff            # встановити глобально
uv tool list                    # список глобальних інструментів
uv tool uninstall ruff          # видалити
uv lock / uv export
Lockfile та сумісність
Генерує або оновлює uv.lock. Може експортувати залежності у формат requirements.txt для інструментів, що не підтримують uv.lock.
uv lock                                          # оновити lockfile
uv export -f requirements-txt > requirements.txt # експорт у pip-формат
uv export --no-dev -f requirements-txt > req-prod.txt

Poetry — зрілий менеджер для складних проектів

Poetry — це інструмент, що вийшов у 2018 році і зараз широко використовується у виробничих проектах. Він вирішує ту саму проблему, що і uv, але з акцентом на управління залежностями з групами і публікацію пакетів на PyPI.

Автоматичне управління середовищем

Poetry автоматично створює та активує venv. Не потрібно python -m venv .venv && source .venv/bin/activate — достатньо poetry install.

poetry.lock — точне відтворення

Lockfile фіксує точні версії і хеші пакетів. poetry install завжди відтворює ідентичне середовище на будь-якій машині або у CI.

Групи залежностей

Окремі групи для dev, test, docs — залежності розробника не потрапляють у production-image при встановленні через poetry install --without dev.

Публікація на PyPI

poetry publish — єдина команда для збірки та публікації пакету. Замінює setuptools, twine і ручне редагування setup.py.

Встановлення Poetry

# Рекомендований спосіб (isolated installer)
curl -sSL https://install.python-poetry.org | python3 -

# macOS через Homebrew
brew install poetry

# Перевірка встановлення
poetry --version  # Poetry (version 1.8.x)

# Важливе налаштування: зберігати venv всередині проекту
# (замість ~/.cache/pypoetry/virtualenvs/)
poetry config virtualenvs.in-project true
poetry config virtualenvs.in-project true — рекомендоване налаштування для більшості розробників. Середовище буде у .venv/ всередині вашого проекту, що зручно для IDE та Docker.

Створення нового проекту

# Новий проект з повною структурою
poetry new my-service

# Або ініціалізація інтерактивно у поточній папці
mkdir existing-project && cd existing-project
poetry init
poetry new — структура нового проекту
$ poetry new my-service
Created package my-service at `my-service`
$ tree my-service/
my-service/
├── my_service/ ← пакет Python
│ └── __init__.py
├── tests/
│ └── __init__.py
├── pyproject.toml ← вся конфігурація тут
└── README.md

pyproject.toml у Poetry

Poetry використовує pyproject.toml — але зі своїми секціями [tool.poetry.*]:

[tool.poetry]
name = "my-service"
version = "0.1.0"
description = "Production-ready REST API"
authors = ["Developer <dev@example.com>"]
readme = "README.md"
packages = [{include = "my_service"}]

[tool.poetry.dependencies]
python = "^3.12"           # версія Python — обов'язкова
fastapi = ">=0.115.0"      # прямі залежності проекту
sqlalchemy = "^2.0"
pydantic = "^2.5"
uvicorn = {extras = ["standard"], version = "^0.32"}

[tool.poetry.group.dev.dependencies]  # група dev — не у production
pytest = "^8.0"
pytest-asyncio = "^0.24"
ruff = "^0.8"
mypy = "^1.13"
black = "^24.0"

[tool.poetry.group.test.dependencies]  # окрема група для тестів
httpx = "^0.28"            # HTTP-клієнт для тестування FastAPI
factory-boy = "^3.3"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Управління залежностями

# Встановлення залежностей з pyproject.toml (також синхронізує lock)
poetry install

# Встановлення без dev-залежностей (для production)
poetry install --without dev

# Додавання нової залежності (оновлює pyproject.toml і lock автоматично)
poetry add fastapi
poetry add "sqlalchemy>=2.0,<3.0"
poetry add uvicorn[standard]          # з extras

# Додавання dev-залежності
poetry add --group dev pytest
poetry add --group dev ruff black mypy

# Видалення залежності
poetry remove redis

# Оновлення всіх залежностей (в межах версійних обмежень)
poetry update

# Оновлення конкретного пакету
poetry update fastapi
poetry install — встановлення та вирішення залежностей
$ poetry install
Installing dependencies from lock file
Package operations: 22 installs, 0 updates, 0 removals
• Installing annotated-types (0.7.0)
• Installing anyio (4.7.0)
Installing fastapi (0.115.6)
• Installing pydantic (2.10.3)
• Installing starlette (0.41.3)
Installing uvicorn (0.32.1)
• Installing pytest (8.3.4) (dev)
• Installing ruff (0.8.4) (dev)
Installing the current project: my-service (0.1.0)

Запуск коду у середовищі Poetry

Poetry не вимагає ручної активації середовища — використовуйте poetry run:

# Запуск скрипта у середовищі проекту
poetry run python main.py

# Запуск застосунку
poetry run uvicorn my_service.main:app --reload

# Запуск тестів
poetry run pytest

# Запуск лінтера
poetry run ruff check .
poetry run mypy my_service/

# Або активувати середовище вручну (як у venv)
poetry shell   # відкриває нову оболонку з активованим середовищем
exit           # повернутись

Довідник команд poetry

poetry new / poetry init
Ініціалізація проекту
poetry new створює проект з готовою структурою папок. poetry init — інтерактивно додає pyproject.toml до існуючої директорії.
poetry new my-service         # новий проект зі структурою
poetry init                   # додати poetry до існуючого проекту
poetry add / poetry remove
Управління залежностями
Додає або видаляє залежності. Автоматично оновлює pyproject.toml та poetry.lock. Підтримує групи, extras та версійні обмеження.
poetry add fastapi                       # остання версія
poetry add "sqlalchemy>=2.0,<3"         # з обмеженнями версій
poetry add uvicorn[standard]             # з extras
poetry add --group dev pytest ruff       # dev-група
poetry add --group test httpx            # test-група
poetry remove redis                      # видалення
poetry install
Встановлення залежностей
Встановлює всі залежності з poetry.lock. Якщо lockfile відсутній — вирішує залежності і створює його.
poetry install                  # встановити всі залежності
poetry install --without dev    # тільки production
poetry install --only test      # тільки вказана група
poetry install --no-root        # без встановлення самого проекту
poetry run
Запуск команд у середовищі
Виконує команду у середовищі проекту без ручної активації. Або відкрийте оболонку через poetry shell.
poetry run python main.py              # запуск скрипта
poetry run uvicorn app:app --reload    # запуск застосунку
poetry run pytest                      # тести
poetry run ruff check .                # лінтер
poetry shell                           # активувати оболонку
poetry update
Оновлення залежностей
Оновлює залежності до найновіших версій у межах обмежень з pyproject.toml. Оновлює poetry.lock.
poetry update              # оновити всі пакети
poetry update fastapi      # оновити конкретний пакет
poetry update --dry-run    # показати що зміниться (без застосування)
poetry show
Перегляд залежностей
Відображає список встановлених пакетів або дерево залежностей з транзитивними пакетами.
poetry show                # список всіх встановлених
poetry show --tree         # дерево залежностей
poetry show fastapi        # деталі конкретного пакету
poetry show --outdated     # застарілі пакети
poetry env
Управління середовищем
Переглядає та управляє віртуальними середовищами Poetry.
poetry env info             # поточне середовище та Python
poetry env list             # всі середовища проекту
poetry env remove 3.12      # видалити середовище
poetry env use 3.11         # перемкнути на іншу версію Python
poetry export
Сумісність з pip
Експортує залежності у requirements.txt для інструментів, що не підтримують pyproject.toml.
poetry export -f requirements.txt --output requirements.txt
poetry export -f requirements.txt --without dev --output req-prod.txt
poetry build / poetry publish
Публікація пакету
Збирає дистрибутив (wheel + sdist) та публікує на PyPI. Замінює setuptools, twine та ручне редагування setup.py.
poetry build                  # зібрати dist/ (wheel + tar.gz)
poetry publish                # опублікувати на PyPI
poetry publish --build        # зібрати і опублікувати одразу
poetry publish --repository testpypi  # на тестовий PyPI

Робота з git: що робити після git clone?

Один із найпоширеніших питань початківців: «Я склонував репозиторій. Що далі?». Відповідь залежить від того, яким менеджером пакетів користується проект.

Як розпізнати менеджер пакетів проекту

Першим ділом — подивіться на файли у корені репозиторію:

uv.lock + pyproject.toml
Проект на uv
Є обидва файли — це uv-проект. Команда для старту: uv sync.
my-project/
├── pyproject.toml   ← залежності та метадані
├── uv.lock          ← детермінований lockfile
├── .python-version  ← зафіксована версія Python
└── src/
poetry.lock + pyproject.toml
Проект на Poetry
Є poetry.lock — це Poetry-проект. Команда для старту: poetry install.
my-project/
├── pyproject.toml   ← залежності та метадані
├── poetry.lock      ← детермінований lockfile
└── src/
requirements.txt
Класичний pip-проект
Тільки requirements.txt — класичний підхід. Команда для старту: pip install -r requirements.txt.
my-project/
├── requirements.txt ← список залежностей
└── app.py

Флоу після git cloneuv-проект

Клонувати та перейти у директорію

git clone https://github.com/org/my-project.git
cd my-project

Перевірити версію Python (опційно)

uv прочитає .python-version автоматично. Якщо потрібна версія відсутня — uv завантажить її сам:

cat .python-version  # наприклад: 3.12
uv python list       # перевірити що встановлено

Синхронізувати середовище

Одна команда — і все готово. uv сам створить .venv/ і встановить точні версії з uv.lock:

uv sync
uv sync після git clone
$ uv sync
Using CPython 3.12.8 interpreter at: /Users/user/.local/share/uv/python/...
Creating virtual environment at: .venv
Resolved 42 packages in 183ms
Installed 42 packages in 1.2s
+ fastapi==0.115.6
+ sqlalchemy==2.0.36
+ ... та інші
All dependencies are satisfied.

Запустити проект

uv run python main.py          # скрипт
uv run uvicorn app:app --reload  # FastAPI
uv run pytest                  # тести
Ніякого source .venv/bin/activateuv run все робить сам. Але якщо хочете активувати середовище явно для IDE або оболонки: source .venv/bin/activate.

Флоу після git clone — Poetry-проект

Клонувати та перейти у директорію

git clone https://github.com/org/my-project.git
cd my-project

Перевірити налаштування Poetry (один раз)

# Рекомендовано: зберігати venv всередині проекту
poetry config virtualenvs.in-project true

Встановити залежності

Poetry прочитає poetry.lock і встановить точні версії:

poetry install
poetry install після git clone
$ poetry install
Installing dependencies from lock file
Package operations: 38 installs, 0 updates, 0 removals
• Installing annotated-types (0.7.0)
Installing fastapi (0.115.6)
• Installing pydantic (2.10.3)
• Installing sqlalchemy (2.0.36)
• ...
Installing the current project: my-project (0.1.0)

Запустити проект

poetry run python main.py
poetry run uvicorn app:app --reload
poetry run pytest

Що комітити у git? Таблиця

Правило просте: lockfile завжди комітити, .venvніколи.

ФайлКомітити?Причина
pyproject.toml✅ ТакМетадані та прямі залежності проекту
uv.lock✅ ТакДетермінований lockfile для відтворення
poetry.lock✅ ТакДетермінований lockfile для відтворення
requirements.txt✅ ТакЯкщо проект класичний pip
.python-version✅ ТакФіксує версію Python для команди
.venv/❌ НіВелика, специфічна для ОС — у .gitignore
__pycache__/❌ НіКеш байткоду — у .gitignore
*.pyc❌ НіКомпільований байткод — у .gitignore
.env❌ НіСекрети та конфіги — у .gitignore

Стандартний .gitignore для Python-проекту

# Віртуальні середовища
.venv/
venv/
env/

# Байткод та кеш
__pycache__/
*.py[cod]
*$py.class
*.pyo

# Артефакти збірки
dist/
build/
*.egg-info/
*.egg

# Тести та покриття
.pytest_cache/
.coverage
htmlcov/

# Типи та лінтери
.mypy_cache/
.ruff_cache/

# Середовище та секрети
.env
.env.local
*.env

# IDE
.idea/
.vscode/
*.swp

Статичний аналіз типів: mypy та Pyright

Python — мова з динамічною типізацією: типи перевіряються під час виконання. Але з Python 3.5+ з'явилися анотації типів (def foo(x: int) -> str:), і на цій основі побудовані статичні аналізатори — інструменти, що знаходять помилки типів до запуску програми.

mypy

Офіційний статичний аналізатор від команди Python. Написаний на Python. Стандарт для більшості проектів, широко підтримується в CI/CD та IDE.

Pyright

Написаний на TypeScript (!) від Microsoft. Значно швидший за mypy. Є основою для type-checking у Pylance (VSCode). Також доступний як CLI — pyright.

Навіщо потрібен статичний аналіз типів?

# Без анотацій — помилка знайдеться лише під час виконання
def get_user_age(user):
    return user["age"] + 1  # KeyError? TypeError? Дізнаємось пізно

# З анотаціями — mypy/Pyright знаходять помилки відразу
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

def get_user_age(user: User) -> int:
    return user["age"] + 1  # mypy: OK
    # return user["nme"] + 1  # mypy: ERROR — key "nme" не існує у User

mypy — встановлення та базове використання

# Встановлення
uv add --dev mypy
# або
pip install mypy

# Запуск для файлу
mypy main.py

# Запуск для всього проекту
mypy .

# Перевірка конкретного пакету
mypy my_package/
mypy — знаходження помилок типів
$ mypy main.py
main.py:12: error: Argument 1 to "add" has incompatible type "str"; expected "int" [arg-type]
main.py:18: error: Item "None" of "Optional[str]" has no attribute "upper" [union-attr]
main.py:25: error: Return type declared as "int", actual return type "str" [return-value]
Found 3 errors in 1 file (checked 1 source file)

Конфігурація mypy у pyproject.toml

[tool.mypy]
python_version = "3.12"
strict = true              # найсуворіший режим — рекомендовано для нових проектів

# Що перевіряти
check_untyped_defs = true  # перевіряти навіть нетиповані функції
disallow_untyped_defs = true  # вимагати анотацій для всіх функцій
disallow_any_generics = true  # заборонити голі Generic (list замість list[str])
warn_return_any = true     # попереджати про return Any

# Що ігнорувати
ignore_missing_imports = true  # якщо бібліотека не має стабів

# Виключення конкретних модулів
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false  # у тестах дозволяємо без анотацій

Практичний приклад: типовані структури даних

from typing import Optional, Union
from collections.abc import Sequence

# TypedDict — словник зі строгою структурою
from typing import TypedDict

class Address(TypedDict):
    street: str
    city: str
    postal_code: str

class UserProfile(TypedDict, total=False):  # total=False — всі поля необов'язкові
    bio: str
    avatar_url: str

class User(TypedDict):
    id: int
    name: str
    email: str
    address: Address
    profile: UserProfile  # вкладений TypedDict

# Функція з повними анотаціями
def find_users_by_city(
    users: Sequence[User],
    city: str,
    limit: Optional[int] = None,
) -> list[User]:
    """Знаходить користувачів із зазначеного міста."""
    result = [u for u in users if u["address"]["city"] == city]
    if limit is not None:
        result = result[:limit]
    return result

# mypy перевірить:
# - що users є Sequence[User]
# - що city є str
# - що limit є Optional[int] (може бути None)
# - що функція повертає list[User]

Pyright — швидший альтернативний аналізатор

# Встановлення через uv
uv add --dev pyright
# або глобально
uvx pyright --version

# Запуск
pyright .
pyright main.py --pythonversion 3.12
pyright — аналіз типів
$ pyright .
Loading configuration file at /Users/user/my-project/pyrightconfig.json
pyright 1.1.391
/my_project/service.py
/my_project/service.py:34:16 - error: Expression of type "str | None" cannot be assigned to declared type "str" (reportAssignmentType)
/my_project/service.py:51:9 - error: Argument of type "int" cannot be assigned to parameter "name" of type "str" (reportArgumentType)
2 errors, 0 warnings, 0 informations
Completed in 0.43s

Конфігурація Pyright — pyrightconfig.json

{
    "pythonVersion": "3.12",
    "venvPath": ".",
    "venv": ".venv",
    "typeCheckingMode": "strict",
    "include": ["src", "tests"],
    "exclude": ["**/__pycache__"],
    "reportMissingImports": true,
    "reportMissingTypeStubs": false,
    "reportUnknownVariableType": false
}

Або у pyproject.toml:

[tool.pyright]
pythonVersion = "3.12"
venvPath = "."
venv = ".venv"
typeCheckingMode = "strict"
include = ["src"]
exclude = ["**/__pycache__"]

Порівняння mypy vs Pyright

ХарактеристикаmypyPyright
Мова реалізаціїPythonTypeScript
Швидкість⚠️ Повільніший на великих проектах⚡ Значно швидший, інкрементальний
Строгість✅ Налаштовувана✅ Налаштовувана
IDE-інтеграціяPyCharm, VSCode (mypy extension)VSCode (Pylance базується на Pyright)
Підтримка стандартів✅ PEP 484, 526, 544...✅ PEP 484, 526, 544...
Plugins✅ Є (Django, SQLAlchemy)⚠️ Менше plugins
CI/CD✅ Стандарт де-факто✅ Зростає
РекомендаціяЗрілі проекти, Django, MLopsFastAPI, нові проекти, VSCode-workflow
FastAPI офіційно рекомендує Pyright (через Pylance у VSCode). Django та більшість MLops-інструментів — mypy з відповідними plugin-ами (django-stubs, sqlalchemy-stubs).

Інтеграція у workflow з uv

# Додавання аналізаторів у dev-залежності
uv add --dev mypy pyright

# Запуск у CI/CD або pre-commit
uv run mypy .
uv run pyright .

# Часто використовують разом з ruff (лінтер)
uv add --dev ruff
uv run ruff check .
uv run ruff format .

pyproject.toml — повна конфігурація типового проекту

[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["fastapi>=0.115", "sqlalchemy>=2.0"]

[dependency-groups]
dev = ["mypy>=1.13", "pyright>=1.1", "ruff>=0.8", "pytest>=8.0"]

[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true

[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "basic"
venvPath = "."
venv = ".venv"

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

Порівняльна таблиця: venv+pip vs uv vs Poetry

Можливістьvenv + pipuvPoetry
Швидкість встановлення🐢 Базова⚡ 10–100x швидше🐇 Швидша за pip
Lockfile❌ Немає (лише freeze)uv.lockpoetry.lock
Управління версіями Python❌ Ні✅ Вбудовано❌ Потрібен pyenv
Групи залежностей❌ Ні✅ dev groups✅ dev/test/docs groups
Автоматичне середовище❌ Вручнуuv runpoetry run
Публікація на PyPI❌ Потрібен twine❌ Не підтримується✅ Вбудовано
Сумісність зі стандартами✅ Стандарт✅ PEP 621⚠️ Частково PEP 621
Навчальна крива✅ Проста✅ Проста⚠️ Середня
Зрілість і поширеність✅ Стандарт🆕 Активно зростає✅ Широко у production

Обирайте venv + pip

Для навчання, простих скриптів, проектів де важлива максимальна сумісність зі стандартними інструментами. Це базовий рівень, який треба знати всім.

Обирайте uv

Для нових проектів будь-якого розміру. Особливо — для CI/CD, де швидкість встановлення залежностей критична. Найкращий вибір для 2024+ проектів.

Обирайте Poetry

Для бібліотек, що публікуються на PyPI, або для команд що вже використовують Poetry і мають налагоджений workflow. Відмінна підтримка у більшості IDE.

Частина V: Стандартна бібліотека — «Батарейки у комплекті»

Філософія "Batteries Included"

Одна з найсильніших сторін Python — філософія "batteries included" (батарейки у комплекті). Це означає, що разом з інтерпретатором ви отримуєте величезний набір готових, оптимізованих та протестованих модулів стандартної бібліотеки. Вони покривають більшість повсякденних завдань розробника: від математичних обчислень до мережевої взаємодії та роботи з файловою системою.

Використання стандартної бібліотеки гарантує:

  • Портативність: ваш код працюватиме на будь-якому комп'ютері, де встановлено Python, без необхідності встановлювати сторонні залежності.
  • Стабільність: ці модулі підтримуються та оновлюються ядром команди розробників Python.
  • Швидкість: багато критичних частин стандартної бібліотеки написані на мові C для максимальної продуктивності.

Розглянемо ключові модулі, які формують основу розробки на Python.


Математичні обчислення: модуль math

Модуль math надає доступ до математичних функцій та констант, визначених стандартом мови C. Він працює виключно з дійсними числами (для комплексних чисел існує окремий модуль cmath).

Константи та округлення

import math

# Ключові константи
print(f"pi: {math.pi}")      # 3.141592653589793
print(f"e: {math.e}")        # 2.718281828459045
print(f"inf: {math.inf}")    # Позитивна нескінченність
print(f"nan: {math.nan}")    # Not a Number (не число)

# Стратегії округлення
value = 5.67
print(f"Округлення вгору (ceil): {math.ceil(value)}")      # 6
print(f"Округлення вниз (floor): {math.floor(value)}")     # 5
print(f"Відкидання дробової частини (trunc): {math.trunc(value)}")  # 5

Степені, корені та логарифми

# Квадратний корінь
print(math.sqrt(25))  # 5.0 (завжди повертає float)

# Найбільший спільний дільник (НСД) та найменше спільне кратне (НСК)
print(math.gcd(48, 64))  # 16
print(math.lcm(48, 64))  # 192 (доступно в Python 3.9+)

# Логарифми
print(math.log(math.e))  # 1.0 (натуральний логарифм)
print(math.log2(1024))   # 10.0
print(math.log10(1000))  # 3.0
Для звичайного округлення до найближчого цілого використовуйте вбудовану функцію round(), яка не потребує імпорту math. Але пам'ятайте, що round() у Python використовує банківське округлення (округлює до найближчого парного числа при закінченні на .5, наприклад round(2.5) буде 2, а round(3.5) буде 4).

Псевдовипадковість: модуль random

Модуль random генерує псевдовипадкові числа за допомогою алгоритму Вихру Мерсенна (Mersenne Twister). Він підходить для моделювання, симуляцій та ігор, але не є безпечним для криптографії (для безпечних токенів або паролів використовуйте модуль secrets).

Генерація значень

import random

# Дійсне число від 0.0 до 1.0
print(f"random(): {random.random()}")

# Ціле число в діапазоні [1, 10] включно
print(f"randint(1, 10): {random.randint(1, 10)}")

# Дійсне число в діапазоні [1.5, 9.5]
print(f"uniform(1.5, 9.5): {random.uniform(1.5, 9.5)}")

# Випадкове парне число від 0 до 100
print(f"randrange(0, 101, 2): {random.randrange(0, 101, 2)}")

Робота з послідовностями

cards = ["Туз", "Король", "Дама", "Валет", "10"]

# Вибір одного випадкового елемента
print(f"Випадкова карта: {random.choice(cards)}")

# Вибір кількох унікальних елементів (без повторень)
print(f"Роздача на двох: {random.sample(cards, 2)}")

# Перемішування списку на місці (модифікує оригінал!)
random.shuffle(cards)
print(f"Перемішана колода: {cards}")

Концепція відтворюваності: random.seed()

Алгоритм генерації є математичним, тому якщо задати початкову точку (seed), послідовність "випадкових" чисел буде абсолютно ідентичною при кожному запуску.

random.seed(100)
print(random.randint(1, 100))  # Завжди виведе 19
print(random.randint(1, 100))  # Завжди виведе 59

random.seed(100)  # Скидаємо в ту саму початкову точку
print(random.randint(1, 100))  # Знову 19!
Використовуйте random.seed() у модульних тестах, коли вам потрібно протестувати логіку, яка залежить від випадкових факторів, щоб забезпечити стабільні та передбачувані результати тестів.

Робота з часом: модуль datetime

Час є однією з найскладніших концепцій у програмуванні через високосні роки, часові пояси та переходи на літній час. Модуль datetime надає класи для обробки дат і часу.

Основні класи

  • datetime.date: представлення дати (рік, місяць, день).
  • datetime.time: представлення часу (година, хвилина, секунда, мікросекунда).
  • datetime.datetime: комбінація дати та часу.
  • datetime.timedelta: тривалість або різниця між двома датами/часом.

Створення та арифметика

import datetime

# Поточні значення
now = datetime.datetime.now()
today = datetime.date.today()
print(f"Зараз: {now}")
print(f"Сьогодні: {today}")

# Створення конкретної дати
deadline = datetime.datetime(2026, 12, 31, 23, 59, 0)

# Розрахунок різниці (timedelta)
time_left = deadline - now
print(f"До дедлайну залишилося: {time_left.days} днів та {time_left.seconds // 3600} годин")

# Додавання інтервалу часу
future_date = today + datetime.timedelta(weeks=4)
print(f"Через 4 тижні буде: {future_date}")

Форматування та парсинг

Для перетворення часу в рядок використовується метод strftime (format time), а для зворотного парсингу з рядка — strptime (parse time).

import datetime
now = datetime.datetime.now()

# Використовуємо спеціальні директиви (специфікатори)
formatted = now.strftime("%Y-%m-%d %H:%M:%S")
print(formatted)  # "2026-06-13 20:30:15"

human_friendly = now.strftime("%d %B %Y, (%A)")
print(human_friendly)  # "13 June 2026, (Saturday)"

Naive vs Aware об'єкти часу

За замовчуванням об'єкти datetime є naive (наївними) — вони не містять інформації про часовий пояс. Це небезпечно для серверних систем, де клієнти можуть бути з різних куточків світу. Об'єкти з інформацією про часовий пояс називаються aware (усвідомленими).

З Python 3.9 вбудовано модуль zoneinfo для зручної роботи з базою даних часових поясів IANA:

from datetime import datetime
from zoneinfo import ZoneInfo

# Створення часу в часовому поясі Києва
kyiv_time = datetime.now(ZoneInfo("Europe/Kyiv"))
print(f"Київ: {kyiv_time}")  # Наприкінці з'явиться зміщення UTC: +03:00 (або +02:00)

# Конвертація в часовий пояс Нью-Йорка
ny_time = kyiv_time.astimezone(ZoneInfo("America/New_York"))
print(f"Нью-Йорк: {ny_time}")  # Час автоматично перераховується

Системна взаємодія: os vs sys vs pathlib

Ці модулі забезпечують взаємодію з операційною системою та інтерпретатором Python.

Порівняння призначення os та sys

  • os: відповідає за роботу з операційною системою (робота з файлами на диску, створення папок, змінні оточення ОС, системні виклики).
  • sys: відповідає за взаємодію з самим інтерпретатором Python (параметри запуску скрипта, керування шляхами імпорту sys.path, внутрішні налаштування пам'яті та лімітів рекурсії).

pathlib — сучасний стандарт для роботи зі шляхами

Раніше для роботи зі шляхами використовувався модуль os.path. Сьогодні рекомендованим є модуль pathlib, який надає об'єктно-орієнтований підхід до шляхів.

from pathlib import Path

# Створення об'єкта шляху
project_dir = Path("/home/user/project")

# Об'єднання шляхів через оператор '/' (дуже зручно!)
file_path = project_dir / "src" / "config.json"
print(file_path)  # /home/user/project/src/config.json

# Перевірки та операції
print(f"Чи існує? {file_path.exists()}")
print(f"Розширення: {file_path.suffix}")
print(f"Ім'я файлу: {file_path.name}")

# Створення папки разом із батьківськими
Path("new_folder/sub_folder").mkdir(parents=True, exist_ok=True)

Ключові можливості sys

import sys

# Аргументи командного рядка (наприклад: python main.py data.txt)
# sys.argv[0] — це завжди ім'я самого скрипта
print(f"Аргументи запуску: {sys.argv}")

# Платформа та версія
print(sys.platform)  # 'darwin' (macOS), 'win32' (Windows), 'linux' (Linux)
print(sys.version)   # Детальна версія інтерпретатора

# Безпечне завершення роботи скрипта з кодом виходу
# 0 = успіх, будь-яке інше число = помилка
sys.exit(0)

Спеціалізовані контейнери: модуль collections

Стандартні типи dict, list, set та tuple чудово підходять для більшості завдань, але модуль collections надає альтернативні контейнери з розширеною поведінкою.

namedtuple — кортеж з іменованими полями

Коли вам потрібен легковажний, незмінний об'єкт (наприклад, для представлення точки на карті чи запису користувача), писати повноцінний клас занадто довго. Звичайний кортеж Point = (50.45, 30.52) змушує звертатися за індексами Point[0], що шкодить читабельності.

from collections import namedtuple

# Створюємо тип (фабрику) Geopoint з полями latitude та longitude
Geopoint = namedtuple("Geopoint", ["latitude", "longitude"])

# Створюємо екземпляр
kyiv = Geopoint(latitude=50.4501, longitude=30.5234)

# Звернення за іменами полів!
print(f"Широта: {kyiv.latitude}, Довгота: {kyiv.longitude}")

# Але це все ще кортеж (сумісність збережена)
print(f"Індекс 0: {kyiv[0]}")
lat, lon = kyiv  # Розпакування працює

defaultdict — словник зі значенням за замовчуванням

При роботі зі звичайним dict звернення до неіснуючого ключа викликає KeyError. defaultdict автоматично створює значення за замовчуванням при першому зверненні до нового ключа.

from collections import defaultdict

# Словник, який за замовчуванням створює порожній список (list) для нових ключів
grouped_students = defaultdict(list)

# Нам не потрібно перевіряти, чи є ключ "Python" у словнику!
grouped_students["Python"].append("Олег")
grouped_students["Python"].append("Марія")
grouped_students["Go"].append("Іван")

print(grouped_students)
# defaultdict(<class 'list'>, {'Python': ['Олег', 'Марія'], 'Go': ['Іван']})

Counter — зручний лічильник елементів

Клас Counter призначений для швидкого підрахунку кількості об'єктів. Він є підкласом dict, тому успадковує всі його методи.

from collections import Counter

words = ["яблуко", "банан", "яблуко", "апельсин", "банан", "яблуко"]
counter = Counter(words)

print(counter)  # Counter({'яблуко': 3, 'банан': 2, 'апельсин': 1})
print(f"Кількість бананів: {counter['банан']}")
print(f"Кількість неіснуючих елементів: {counter['груша']}")  # Повертає 0 замість KeyError!

# Отримання топ-N найпопулярніших елементів
print(f"Найпопулярніші: {counter.most_common(2)}")  # [('яблуко', 3), ('банан', 2)]

deque — оптимізована двостороння черга

Звичайний список Python list оптимізований для операцій у кінці структури. Додавання або видалення елемента з початку списку (list.insert(0, val) або list.pop(0)) має складність $O(N)$, оскільки всі інші елементи в пам'яті мають зсунутися на один крок.

deque (double-ended queue) реалізований як двобічно зв'язаний список, що робить додавання та видалення елементів з обох кінців надзвичайно швидким — за $O(1)$.

from collections import deque

queue = deque(["Користувач 1", "Користувач 2"])

# Швидке додавання в кінець
queue.append("Користувач 3")

# Швидке додавання на початок
queue.appendleft("VIP Користувач")
print(queue)  # deque(['VIP Користувач', 'Користувач 1', ...])

# Видалення з обох кінців
first_in = queue.popleft()  # 'VIP Користувач'
last_in = queue.pop()       # 'Користувач 3'
Використовуйте deque для реалізації стеків, черг завдань (FIFO/LIFO) або для зберігання логів фіксованого розміру (задавши параметр maxlen при створенні, наприклад deque(maxlen=100) — при додаванні 101-го елемента перший автоматично видалиться).

Повний цикл роботи: від нуля до готового проекту

Це стандартна послідовність дій при створенні нового Python-проекту:

# 1. Створити директорію проекту
mkdir my-awesome-project
cd my-awesome-project

# 2. Ініціалізувати git-репозиторій (опціонально, але рекомендується)
git init

# 3. Створити файл .gitignore — ОДРАЗУ, до будь-яких комітів
cat > .gitignore << 'EOF'
# Віртуальне середовище — ніколи не комітимо!
.venv/
venv/

# Байткод Python
__pycache__/
*.pyc
*.pyo

# Середовища та IDE
.env
.DS_Store
.idea/
.vscode/
EOF

# 4. Створити та активувати venv
python -m venv .venv
source .venv/bin/activate  # (.venv) з'явиться у промпті

# 5. Встановити залежності проекту
pip install requests django pytest

# 6. Зафіксувати залежності
pip freeze > requirements.txt

# 7. Написати код...

# 8. Перед завершенням — оновити requirements.txt
pip freeze > requirements.txt
git add requirements.txt
git commit -m "chore: update dependencies"
Критично важливо: ніколи не комітьте директорію .venv/ у Git-репозиторій. Вона містить сотні файлів і повністю залежить від конкретної машини та версії Python. Замість цього комітьте requirements.txt — це єдине, що потрібно колезі або CI/CD для відтворення вашого середовища.

Практичний приклад від А до Я: CLI-інструмент завантаження та оптимізації зображень

Щоб об'єднати всі вивчені концепції (модулі, пакети, venv, sys.path, sys.modules, sys.argv, Counter та pathlib) в єдине ціле, створимо реальний виробничий інструмент командного рядка — Image Processor CLI.

Постановка задачі

Нам потрібно написати утиліту, яка:

  1. Приймає текстовий файл зі списком URL-адрес зображень.
  2. Створює ізольовану вихідну директорію для збереження результатів.
  3. Завантажує зображення з мережі (потребує стороннього пакета requests).
  4. Змінює розмір зображень до заданої ширини та конвертує їх у сучасний оптимізований формат WebP (потребує стороннього пакета Pillow).
  5. Збирає детальну статистику про хід виконання (успіх, помилка мережі, помилка обробки) за допомогою collections.Counter та повертає відповідний код виходу в систему.

Архітектура проекту

Проект буде організований як повноцінний пакет Python з такою структурою директорій:


Пакетна реалізація проекту крок за кроком

Налаштування директорії та ізоляції (venv)

Створимо структуру папок та ініціалізуємо ізольоване віртуальне середовище, щоб сторонні пакети requests та Pillow не конфліктували з іншими версіями у глобальній системі.

Ініціалізація оточення
$ mkdir -p my_project/image_processor/core my_project/image_processor/utils
$ cd my_project
$ python3.12 -m venv .venv
$ source .venv/bin/activate
(.venv) $ which python
/Users/user/my_project/.venv/bin/python

Встановлення та фіксація залежностей

Встановимо необхідні сторонні бібліотеки через pip та запишемо їх у requirements.txt.

Встановлення залежностей через pip
(.venv) $ pip install requests Pillow
Collecting requests...
Collecting Pillow...
Successfully installed requests-2.31.0 Pillow-10.2.0 ...
(.venv) $ pip freeze > requirements.txt

Створення вихідних файлів коду

Запишіть код для кожного з файлів структури проекту image_processor відповідно до архітектурного опису, наведеного вище.

Зверніть увагу на відносні імпорти у файлі image_processor/__init__.py: from .core.downloader import download_image Це гарантує локальну цілісність пакета. Натомість у файлі main.py, який виступає точкою запуску, використовуються абсолютні імпорти: from image_processor import process_images оскільки при запуску через python -m точка входу має викликатися з кореня sys.path.

Підготовка тестових даних

Створимо файл urls.txt в корені проекту my_project/ зі списком реальних тестових зображень для завантаження:

# urls.txt
# Список зображень для оптимізації
https://images.unsplash.com/photo-1579783900882-c0d3dad7b119?w=1200
https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=1200
# Невалідний URL для тестування стійкості до помилок:
https://non-existent-domain-xyz.com/image.jpg

Запуск та аналіз роботи CLI

Запустимо наш пакет із кореневої папки проекту за допомогою прапорця інтерпретатора -m. Ми вкажемо вхідний файл urls.txt, папку збереження optimized_images та бажану ширину 600 пікселів.

Запуск утиліти
(.venv) $ python -m image_processor.main urls.txt ./optimized_images 600
Знайдено 2 посилань. Починаємо обробку...
Збережено оптимізоване зображення: optimized_images/image_1.webp
Збережено оптимізоване зображення: optimized_images/image_2.webp
Помилка мережі при завантаженні https://non-existent-domain-xyz.com/image.jpg...
=== Статистика обробки ===
Успішно оброблено: 2
Помилок мережі: 1
Помилок обробки: 0

Перевірка результатів на диску

Перевіримо вміст автоматично створеної директорії optimized_images за допомогою pathlib-подібних команд або звичайної утиліти перегляду.

Перевірка результатів
(.venv) $ ls -l optimized_images/
-rw-r--r-- 1 user staff 42104 Jun 13 20:45 image_1.webp
-rw-r--r-- 1 user staff 38942 Jun 13 20:45 image_2.webp
← Зображення успішно стиснені, зменшені та збережені у форматі WebP!

Порівняльна таблиця: стратегії імпорту

СинтаксисПростір іменРизик конфліктуЧитабельністьРекомендація
import moduleізольований❌ мінімальний✅ висока✅ основний підхід
from module import nameпоточний⚠️ є⚠️ середня✅ для 1-3 об'єктів
import module as aliasізольований❌ мінімальний✅ висока (якщо alias відомий)✅ для довгих імен
from module import *поточний❌❌ гарантований❌ низька❌ уникати завжди

Підсумки та найкращі практики

Модулі

Кожен .py-файл — модуль. Ділить код на логічні одиниці. Завантажується один раз, кешується у sys.modules. Пишіть if __name__ == "__main__" для двоїстих файлів.

sys.path і sys.modules

sys.path — список директорій для пошуку. sys.modules — кеш завантажених модулів. Розуміння цих двох структур пояснює більшість «магії» імпорту.

Пакети

Директорія з __init__.py. Організує модулі ієрархічно. __init__.py слугує фасадом публічного API. __all__ документує контракт модуля.

venv + pip

Базовий стандарт ізоляції, вбудований у Python. Вивчіть його першим — він підвалина для розуміння того, як працюють uv та Poetry.

uv

Рекомендований вибір для нових проектів. У 10–100 разів швидший за pip, управляє версіями Python, має lockfile та не потребує ручної активації mid venv.

Poetry

Ідеальний для публікації бібліотек на PyPI та для команд з розвиненим workflow. Чіткі групи залежностей (dev/test/docs) і зрілий tooling навколо.

Золоті правила, що варто запам'ятати:

  1. Завжди використовуйте ізольоване середовище для кожного проекту (venv, uv venv або Poetry)
  2. Завжди додавайте .venv/ у .gitignore
  3. Завжди фіксуйте залежності: pip freeze > requirements.txt або через lockfile (uv.lock / poetry.lock)
  4. ✅ Для нових проектів — обирайте uv (швидкість + простота) або Poetry (якщо плануєте публікацію на PyPI)
  5. ✅ Використовуйте import module або from module import name, уникайте from module import *
  6. ✅ Пишіть if __name__ == "__main__": у кожному файлі, що може запускатися напряму
  7. ✅ Структуруйте код у пакети при наявності більш ніж 5–7 модулів
  8. ✅ Використовуйте абсолютні імпорти між пакетами, відносні — всередині одного пакета
Copyright © 2026