Привет, Хабр!

Если вы собираете прототип на C++, то один файл с main.cpp иногда реально компилируется в рабочую утилиту. Библиотеки либо завозятся пакетным менеджером заранее, либо у вас есть header‑only зависимость и всё взлетает. В Python долгое время это было болью: любой однофайловый скрипт, который требует requests или rich, уже тянет за собой виртуальные окружения, инструкции в README и локальные фичи.

Есть рабочий стандарт для нормальных однофайловых сценариев с зависимостями — PEP 723: вы объявляете зависимости прямо в комментариях, а раннер ставит всё сам и запускает в изолированной среде. В связке с uv получается неплохой такой способ делиться скриптами, в том числе для пвспомогательных задач. И да, у этой красоты есть нюансы безопасности, о них поговорим отдельно.

Что такое PEP 723 и как это выглядит

PEP 723 определяет блок метаданных в комментариях, который парсится внешними инструментами. Формат прост: сверху и снизу маркеры, внутри TOML c полями dependencies и requires-python.

Пример минимального скрипта:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "httpx<1.0",
#   "rich>=13.7",
# ]
# ///
import httpx
from rich import print

def main() -> None:
    r = httpx.get("https://httpbin.org/json", timeout=10)
    r.raise_for_status()
    print({"status": r.status_code, "len": len(r.text)})

if __name__ == "__main__":
    main()

Запуск через uv:

uv run example.py

uv создаст изолированную среду в кеше, установит зависимости и выполнит код. Никаких ручных venv.

Добавляем и управляем зависимостями прямо из CLI

PEP 723 — это про формат, но править TOML руками быстро надоедает. uv умеет редактировать блок зависимостей в файле:

uv add --script example.py "pydantic>=2.8"
uv remove --script example.py "httpx"
uv run example.py

Если нужен исполняемый файл без явного вызова uv run, используем shebang:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["rich"]
# ///
from rich import print
print("hello")

Ставим права и запускаем из каталога:

chmod +x greet
./greet

Лочим зависимости и делаем скрипт воспроизводимым

Одно дело установить актуальные версии, другое — зафиксировать их. uv поддерживает lock‑файл для скриптов.

uv lock --script example.py
# появится example.py.lock

# теперь любые операции будут учитывать lock:
uv run --script example.py
uv add --script example.py "rich"      # обновит lock
uv export --script example.py -o req.txt

Можно ограничить свежесть пакетов датой, чтобы избежать неожиданных апдейтов через год. В блоке [tool.uv] внутри метаданных:

# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx<1.0"]
# [tool.uv]
# exclude-newer = "2025-08-01T00:00:00Z"
# ///

exclude-newer заставит резолвер игнорировать релизы, вышедшие после указанной даты.

А если нужно именно pip‑совместимое зафиксированное дерево для длительных CI‑пайплайнов, у uv есть инструменты уровня pip-tools: uv pip compile и uv pip sync. Они позволяют сгенерировать requirements.txt из исходника‑декларации и синхронизировать окружение один‑в‑один.

Контроль версии Python на рантайме

Скрипт может требовать конкретную ветку интерпретатора. В этом случае uv позволяет запросить нужную версию при запуске:

uv run --python 3.10 example.py
uv run --python 3.12 example.py

Безопасность

Однофайловые скрипты удобно запускать как есть, и именно поэтому надо быть аккуратнее обычного. Риски:

  1. Подмена зависимостей. Тривиальные атаки на цепочку поставок через зарегистрированные пакеты с похожими именами.

  2. Непредсказуемые апгрейды. Сегодня всё чисто, завтра новый релиз транзитивной зависимости ломает инварианты.

  3. Источники пакетов. Переопределение индексов, отключение TLS‑проверок, внутренние зеркала без политики.

  4. Уязвимости в уже выбранных версиях.

Конкретные меры:

Фиксация и аудит. Для скриптов создаём example.py.lock и регулярно прогоняем аудит зависимостей. В экосистеме PyPA для этого есть pip-audit, он использует базу уязвимостей PyPI и может работать поверх вашего requirements или установленной среды. Можно запускать через uvx в изолированном окружении:

uvx pip-audit -r req.txt
# или просканировать локальную среду
uvx pip-audit --local

Проект поддерживается PyPA, есть GitHub Action.

Хеши и синхронизация. Для больших пайплайнов можно генерировать requirements с хешами, а устанавливать через uv pip sync, чтобы окружение соответствовало файлу один к одному. Управление хешами и параметры компиляции конфигурируются в uv.toml/pyproject.toml для uv pip compile.

Индексы и TLS. Не используем доверенные небезопасные хосты. В справочнике CLI прямо есть предупреждение: флаг --allow-insecure-host отключает верификацию цепочки сертификатов, что делает вас уязвимыми для MITM.

Воспроизводимость по времени. Добавляем exclude-newer к скрипту, держите lock рядом с ним, а CI запускайте с uv run --script или с экспортом в requirements.txt и последующим uv pip sync.

Сценарий: маленький CLI-интеграционный скрипт

Условие: нужен однофайловый инструмент для выгрузки данных из API, с ретраями и логами, без установки проекта и с воспроизводимыми версиями.

fetch_users.py:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "httpx==0.27.2",
#   "tenacity==9.0.0",
#   "structlog==24.4.0",
# ]
# [tool.uv]
# exclude-newer = "2025-08-01T00:00:00Z"
# ///
from __future__ import annotations

import os
import sys
import json
import time
import httpx
import structlog
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

log = structlog.get_logger()

API_URL = os.environ.get("API_URL", "https://httpbin.org/json")
TIMEOUT = httpx.Timeout(10.0, connect=5.0)

class FetchError(RuntimeError):
    pass

@retry(
    reraise=True,
    stop=stop_after_attempt(4),
    wait=wait_exponential(multiplier=0.5, min=0.5, max=5),
    retry=retry_if_exception_type((httpx.HTTPError, FetchError)),
)
def fetch_json(client: httpx.Client, url: str) -> dict:
    r = client.get(url)
    if r.status_code >= 500:
        # серверная ошибка — пробуем повторить
        raise FetchError(f"server_error={r.status_code}")
    r.raise_for_status()
    return r.json()

def main() -> int:
    structlog.configure(processors=[structlog.processors.add_log_level, structlog.processors.JSONRenderer()])
    with httpx.Client(timeout=TIMEOUT, headers={"User-Agent": "fetch-users/1.0"}) as client:
        t0 = time.perf_counter()
        data = fetch_json(client, API_URL)
        dt = time.perf_counter() - t0
        log.info("fetched", bytes=len(json.dumps(data).encode("utf-8")), seconds=round(dt, 3))
        print(json.dumps(data, ensure_ascii=False))
    return 0

if __name__ == "__main__":
    try:
        sys.exit(main())
    except httpx.RequestError as e:
        log.error("http_error", error=str(e))
        sys.exit(2)
    except Exception as e:
        log.error("unexpected", error=str(e.__class__.__name__))
        sys.exit(3)

Запуск:

uv run fetch_users.py
# закрепляем версии рядом со скриптом
uv lock --script fetch_users.py
# экспорт для CI
uv export --script fetch_users.py -o requirements.txt
uvx pip-audit -r requirements.txt

uv задокументировал и lock для скриптов, и экспорт.

Инспекция дерева зависимостей и политика источников

Перед выкладкой в хук pre‑commit можно обозреть дерево:

uv tree --script fetch_users.py

Для частных индексов и зеркал используем конфигурационные файлы uv.toml или pyproject.toml для uv pip и не раздаем --allow-insecure-host. Конфиги поддерживаются на уровне проекта и пользователя; есть и системный уровень. Путь к кешу и параметры можно контролировать через команды и переменные окружения.


Итог

PEP 723 решает рутину: однофайловые скрипты могут быть самодостаточными, без вопросов о том, как установить зависимости. uv сделал это быстрым и операционно удобным: редактирование зависимостей, shebang, lock рядом со скриптом, экспорт для CI, контроль версии интерпретатора.

Как и в случае с однофайловыми скриптами по PEP 723 и uv, которые позволяют быстро проверить идею без лишних подготовительных шагов, у вас есть возможность так же познакомиться с курсом Python Developer. Professional — через бесплатные открытые уроки, доступные по ссылке.

Кроме того, вы можете пройти бесплатное вступительное тестирование, которое позволит оценить ваши знания и навыки.

А если хотите узнать больше о самом курсе и впечатлениях участников, загляните в секцию с отзывами.

Комментарии (1)


  1. high_panurg
    27.08.2025 06:52

    А почему бы просто не собрать nuitka один бинарник и не мучаться?