Привет, Хабр!
Если вы собираете прототип на 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
Безопасность
Однофайловые скрипты удобно запускать как есть, и именно поэтому надо быть аккуратнее обычного. Риски:
Подмена зависимостей. Тривиальные атаки на цепочку поставок через зарегистрированные пакеты с похожими именами.
Непредсказуемые апгрейды. Сегодня всё чисто, завтра новый релиз транзитивной зависимости ломает инварианты.
Источники пакетов. Переопределение индексов, отключение TLS‑проверок, внутренние зеркала без политики.
Уязвимости в уже выбранных версиях.
Конкретные меры:
Фиксация и аудит. Для скриптов создаём 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 — через бесплатные открытые уроки, доступные по ссылке.
Кроме того, вы можете пройти бесплатное вступительное тестирование, которое позволит оценить ваши знания и навыки.
А если хотите узнать больше о самом курсе и впечатлениях участников, загляните в секцию с отзывами.
high_panurg
А почему бы просто не собрать nuitka один бинарник и не мучаться?
pda0
Потому что ценность скриптов перед исполняемыми файлами в том, что их можно посмотреть, подправить и т.д.
Ваш дежурный Капитан Очевидность.