표준 라이브러리만 쓰는 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 requestsInitialized 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 requestsSuccessfully 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 –script | pip –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 -Sshebang을 쓰지 마십시오.uv run --script pypi-latest.py처럼 명령으로 실행하도록 안내합니다. - 운영 환경용 스크립트를
>=하한만으로 배포하지 마십시오.exclude-newer나.lock파일로 고정합니다. exclude-newer를 pip 경로에서 기대하지 마십시오. uv만 읽는 설정입니다..lock파일을 스크립트와 함께 복사하십시오. lock 파일은 스크립트 밖에 있으므로 단일 파일 배포의 장점이 줄어듭니다.
마무리
PEP 723 블록을 넣으면 의존성 정보가 스크립트와 함께 이동합니다. uv가 있으면 실행까지 자동이고, pip 26 이상이면 설치를 표준 도구로 할 수 있습니다. 남은 과제는 shebang 이식성과 스크립트 안의 버전 고정입니다.