PEP 723로 의존성을 품은 Python 스크립트 — uv run –script와 pip –requirements-from-script

표준 라이브러리만 쓰는 Python 스크립트는 다른 머신에 복사해도 바로 실행됩니다. requests 하나만 써도 상황이 달라집니다. 전역 pip install은 환경을 어지럽힙니다. Ubuntu처럼 externally-managed 표시가 된 Python은 이를 막습니다. 가상 환경을 쓰면 머신마다 다시 만들어야 합니다.

PEP 723(Inline script metadata)은 의존성을 스크립트 안에 적는 표준입니다. LWN에 트레이 허너(Trey Hunner)가 기고한 글을 바탕으로, 이 표준을 uv와 pip 26으로 쓰는 방법을 실행해 확인합니다. shebang 이식성과 버전 고정 문제도 함께 다룹니다.

PEP 723 메타데이터 형식

# /// script와 # /// 사이의 주석은 TOML입니다. requires-python과 dependencies 키는 uv가 아니라 PEP 723이 정의합니다. 그래서 pipx, pip-run, Hatch, PDM, pex도 같은 블록을 읽습니다.

시기사건
2016제이슨 쿰스(Jason Coombs)가 pip-run(당시 rwt) 공개
2023년 7월폴 무어(Paul Moore)가 PEP 722 제안
2023년 10월브렛 캐넌(Brett Cannon)이 TOML 기반 PEP 723을 조건부 승인
2024년 8월uv가 PEP 723 지원
2026년 1월pip 26.0에 --requirements-from-script 추가

uv로 실행하기

실습 환경은 Ubuntu 24.04, Python 3.12.3, uv 0.13.7입니다. uv init --script가 빈 메타데이터 블록을 만들고, uv add --script가 의존성을 추가합니다.

uv init --script pypi-latest.py --python 3.12
uv add --script pypi-latest.py requests
Initialized script at `pypi-latest.py`
Resolved 5 packages in 253ms

생성된 본문을 실제 코드로 바꾸고 맨 위에 shebang을 넣습니다.

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "requests>=2.34.2",
# ]
# ///
"""PyPI 패키지별 최신 버전을 출력한다."""
import sys

import requests

for name in sys.argv[1:]:
    r = requests.get(f"https://pypi.org/pypi/{name}/json", timeout=10)
    print(name, r.json()["info"]["version"])
$ chmod +x pypi-latest.py
$ time ./pypi-latest.py django flask      # 1회차
Installed 5 packages in 5ms
django 6.1.3
flask 3.1.3

real	0m0.563s

$ time ./pypi-latest.py django flask      # 2회차
django 6.1.3
flask 3.1.3

real	0m0.303s

1회차에서 uv가 가상 환경을 만들고 패키지를 설치합니다. 2회차부터는 같은 환경을 재사용하고 버전만 확인합니다. 시간 대부분은 PyPI 응답 대기입니다.

uv cache dir
uv python find --script pypi-latest.py
/home/noble/.cache/uv
$UV_CACHE_DIR/environments-v2/pypi-latest-df9072ef776586f4/bin/python

환경은 uv 캐시의 environments-v2 아래에 있습니다. 실습에서는 UV_CACHE_DIR로 캐시 위치를 바꿨습니다. 캐시를 지우면 다음 실행에서 다시 만듭니다.

pip만으로 실행하기

uv가 없는 환경에서는 pip 26.0 이상의 --requirements-from-script를 씁니다. 가상 환경 생성과 shebang 경로는 직접 관리해야 합니다.

python3 -m venv --upgrade-deps ~/.venvs/pypi-latest
~/.venvs/pypi-latest/bin/python -m pip install \
    --requirements-from-script pypi-latest.py

# shebang을 가상 환경의 python 절대 경로로 바꾼다
sed -i "1s|.*|#!$HOME/.venvs/pypi-latest/bin/python|" pypi-latest.py
./pypi-latest.py requests
Successfully installed certifi-2026.10.23 charset_normalizer-3.5.2 idna-3.20 requests-2.34.2 urllib3-2.8.0
requests 2.34.2

shebang에 절대 경로가 들어가므로 다른 머신에서는 같은 경로에 가상 환경을 다시 만들어야 합니다.

항목uv run –scriptpip –requirements-from-script
가상 환경 생성자동직접
메타데이터 변경 반영실행할 때마다 확인pip install을 다시 실행해야 반영
스크립트 실행uv가 실행shebang의 python이 실행
새 머신에 필요한 것uv 설치같은 경로의 가상 환경
의존성 해석 시점새 머신에서 첫 실행 때pip install을 실행할 때만

shebang의 env -S 이식성

대부분의 커널은 shebang에서 인터프리터 뒤의 문자열 전체를 인자 하나로 넘깁니다. -S를 빼면 env가 “uv run –script”라는 이름의 프로그램을 찾습니다.

$ head -1 noS.py
#!/usr/bin/env uv run --script
$ ./noS.py requests
/usr/bin/env: ‘uv run --script’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines

-S는 표준이 아닙니다. GNU coreutils 8.30, FreeBSD 6.0, macOS 10.11부터 지원합니다. Alpine의 기본 env인 BusyBox는 지원하지 않습니다.

$ busybox env -S 'echo hi'
env: invalid option -- 'S'
BusyBox v1.36.1 (Ubuntu 1:1.36.1-6ubuntu3.1) multi-call binary.

의존성 버전 고정

uv add는 >= 하한만 적습니다. 새 머신에서 처음 실행하면 그 시점의 최신 버전을 받습니다. 2026년 3월과 10월의 LiteLLM 사례처럼 오염된 릴리스가 올라와 있으면 그대로 설치됩니다. ==로 고정해도 전이 의존성은 고정되지 않습니다.

스크립트 안에서 쓸 수 있는 방법은 [tool.uv]의 exclude-newer입니다. 지정한 시각 이후에 올라온 배포 파일을 무시합니다.

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["requests"]
#
# [tool.uv]
# exclude-newer = "2025-01-01T00:00:00Z"
# ///
import requests
print(requests.__version__)
$ ./pinned.py
Installed 5 packages in 5ms
2.32.3

최신 버전은 2.34.2이지만 2025년 1월 1일 기준 최신인 2.32.3이 설치됐습니다. 전이 의존성까지 정확히 고정하려면 lock 파일을 따로 만듭니다.

$ uv lock --script pypi-latest.py
Resolved 5 packages in 2ms
$ ls
pypi-latest.py  pypi-latest.py.lock

Python 자체 지원 논의

PEP 723은 실행 도구를 정의하지 않습니다. Python에 기본 지원을 넣으려면 인터프리터가 스크립트별 가상 환경 생성, 의존성 설치, 실행을 모두 맡아야 합니다. pip 메인테이너 데미언 쇼(Damian Shaw)는 더 빠른 의존성 해석기를 프로토타입으로 만들고 있습니다. 이 문제는 9월에 선출된 Python 패키징 위원회가 다룰 수 있습니다.

주의사항

  • Alpine, OpenBSD, NetBSD에 배포할 스크립트에는 env -S shebang을 쓰지 마십시오. uv run --script pypi-latest.py처럼 명령으로 실행하도록 안내합니다.
  • 운영 환경용 스크립트를 >= 하한만으로 배포하지 마십시오. exclude-newer나 .lock 파일로 고정합니다.
  • exclude-newer를 pip 경로에서 기대하지 마십시오. uv만 읽는 설정입니다.
  • .lock 파일을 스크립트와 함께 복사하십시오. lock 파일은 스크립트 밖에 있으므로 단일 파일 배포의 장점이 줄어듭니다.

마무리

PEP 723 블록을 넣으면 의존성 정보가 스크립트와 함께 이동합니다. uv가 있으면 실행까지 자동이고, pip 26 이상이면 설치를 표준 도구로 할 수 있습니다. 남은 과제는 shebang 이식성과 스크립트 안의 버전 고정입니다.

참고

답글 남기기