5P by GN⁺ | ★ favorite | 댓글 1개
  • 작은 API 테스트 스크립트도 의존성 때문에 실행 환경을 맞춰야 하는데, uv 실행 shebang을 쓰면 설치 절차 없이 바로 실행 가능한 형태로 만들 수 있음
  • 예시 jam_users.py는 Go API의 /users 엔드포인트를 테스트하기 위해 httpx, IPython, loguru로 사용자 삭제·생성 후 REPL에 진입함
  • 기존 방식은 시스템 Python에 패키지를 전역 설치하거나 가상환경을 직접 준비해야 해, 스크립트 공유와 재실행이 번거로움
  • # /// script 헤더에 dependencies = ["ipython", "httpx", "loguru"]를 선언하고 uv run jam_users.py를 실행하면 uv가 격리 환경과 의존성 설치를 처리함
  • #!/usr/bin/env -S uv run --script shebang과 실행 권한을 더하면, Unix 시스템에서 uv만 설치된 상태./jam_users.py처럼 실행할 수 있음

스크립트 안에 의존성 선언하기

  • 예시 jam_users.py는 로컬 API http://localhost:4000/v1/users를 대상으로 테스트 사용자 데이터를 준비함
    • httpx로 API 요청을 보냄
    • IPython REPL에 진입해 응답을 살펴보고 테스트를 이어감
    • loguru로 삭제 및 생성 로그를 남김
  • 기본 스크립트는 사용자 목록을 초기화한 뒤 테스트 데이터를 다시 채우는 흐름임
    • GET /v1/users로 기존 사용자 목록을 가져옴
    • 각 사용자에 대해 DELETE /v1/users/{id}를 호출함
    • 준비된 사용자 목록을 POST /v1/users에 JSON으로 전송함
    • 이후 IPython.embed()로 REPL을 띄움
  • python jam_users.py로 실행하려면 httpx, IPython, loguru가 실행 환경에 이미 설치되어 있어야 함
  • 시스템 Python에 전역 설치하거나 가상환경을 따로 만드는 방법도 있지만, 둘 다 실행 전 준비가 필요함
    • 전역 설치는 시스템 Python을 패키지로 어지럽힐 수 있음
    • 가상환경 방식은 생성, 활성화, 설치, 실행 과정을 직접 관리해야 함
    • 두 방식 모두 필요한 패키지와 호환되는 시스템 Python이 있어야 함

uv로 직접 실행 가능한 스크립트 만들기

  • uv# /// script 태그를 스크립트 상단에 넣으면 파일 안에서 의존성을 선언할 수 있음
# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • 이 헤더가 있으면 uv run jam_users.py로 실행할 수 있음
    • uv가 스크립트용 격리 가상환경을 생성함
    • 필요한 의존성을 다운로드하고 설치함
    • 해당 가상환경 맥락에서 스크립트를 실행함
  • 일반 Python shebang인 #!/usr/bin/env python은 Python이 # /// script 주석을 무시하므로 uv의 스크립트 헤더를 활용하지 못함
  • shebang에 uv 호출을 직접 넣으면 스크립트를 실행 파일처럼 다룰 수 있음
#!/usr/bin/env -S uv run --script

# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • env-S 플래그는 뒤따르는 문자열을 별도 인자로 나눠 env에 전달하도록 함
  • chmod +x jam_users.py로 실행 권한을 부여한 뒤에는 다음처럼 직접 실행 가능함
./jam_users.py
  • 이 방식은 Unix 시스템에 uv가 설치되어 있으면 별도의 의존성 설치나 가상환경 관리 없이 스크립트를 실행할 수 있음
  • 복잡한 Python 스크립트를 다른 사용자에게 전달할 때, 실행 전 시스템 준비 절차를 길게 안내해야 하는 부담을 줄일 수 있음

댓글과 토론

Hacker News 의견들
  • UV 자체가 아니라 전반적으로, 주석으로 코드 실행을 제어하는 방식이 거슬림
    린터 지시나 개발자 메모에 주석을 쓰는 건 괜찮지만, 설정이나 실행 관련 데이터라면 UV_ENV = { "dependencies": { "requests": "2.32.3", "pandas": "2.2.3" } } 같은 형태가 훨씬 낫다고 봄
    이 방식은 유효한 Python 문법이고, 임의의 주석 파싱 대신 표준 자료구조를 써서 생성과 검증이 쉬우며, 무엇보다 주석을 전부 제거해도 코드가 동일하게 실행되어야 한다는 원칙을 지킴

    • 동의하지만 한 단계 더 나아가고 싶음
      제안한 방식도 런타임에는 아무 일도 하지 않는 매직 상수라서 정적 분석으로만 파싱되며, uv가 읽는 대신 다른 도구가 미사용 코드로 보고 지워버릴 수도 있음
      차라리 import uv 뒤에 uv.exec(dependencies=["clown"], python=">=3.10")처럼 uv에 직접 무엇을 할지 호출하게 만들면 됨
      첫 실행은 이 가상의 uv 패키지를 찾을 수 있는 아무 Python 런타임에서 하고, uv 패키지가 가상 환경과 Python 런타임을 준비한 뒤 환경 변수 같은 플래그를 붙여 re-exec(3)하면 됨
      두 번째 런타임에서는 플래그를 감지한 uv.exec가 아무 작업도 하지 않으면 됨
    • 이건 uv가 발명한 게 아니라, 다른 도구들처럼 표준 PEP 723을 사용하는 것임
      https://peps.python.org/pep-0723/
    • 타당하지만, 명령형 코드를 파싱하고 평가하는 건 최소 권한 원칙에 맞춰 선언적 데이터로 두는 것보다 훨씬 어렵고 덜 유연하다고 봄
      또 shebang 줄 자체도 사실은 주석으로 동작함
      45년 동안 너무 깊게 자리 잡아서 사람들이 그것이 셸 주석이라는 사실을 잘 의식하지 않을 뿐임
    • “주석을 제거해도 코드가 동일하게 실행되어야 한다”는 조건은 여전히 맞음
      같은 의존성을 설치했다면 코드는 동일하게 실행됨
      코드 자체의 의미를 바꾼다기보다 코드가 실행되는 환경을 바꾸는 것이고, 그런 면에서는 셸 스크립트 맨 위의 #!/bin/bash 주석과 다르지 않음
    • 완전히 동의하며, 이런 형태가 결국 표준화되길 바람
      다만 uv는 의존성을 알아내려고 코드를 실행하고 싶지 않을 가능성이 높아서, Python 문법의 매우 제한된 부분집합이어야 할 것임
      애초에 이런 게 필요하다는 사실은 언어의 약점을 드러냄
      import 문 자체가 의존성에 관한 모든 정보를 전달할 수 있어야 함
  • 최근 몇 달 사이 HN에서 이 주제가 아주 자주 나왔고, 최근 예시는 다음과 같음
    https://news.ycombinator.com/item?id=43500124
    https://news.ycombinator.com/item?id=42463975
    uv는 좋아하지만 self-contained라는 표현에는 두 가지 점에서 동의하기 어려움
    첫째, 스크립트를 실행하려면 uv가 이미 설치되어 있어야 함
    셸 스크립트로 uv 설치 여부를 확인하고 없으면 curlpipe로 설치하게 만들 수는 있겠지만, 보일러플레이트가 꽤 늘어나고 curlpipe 방식 자체도 좋지 않음
    둘째, 홈 디렉터리 어딘가에 가상 환경을 자동 생성하는 건 진짜 self-contained가 아님
    일회성으로 실행하고 스크립트를 지워도 그 가상 환경은 남아서 공간을 차지하며, uv 문서에서 이런 임시 가상 환경을 자동으로 정리한다는 보장을 찾지 못했음

    • uv가 필요하고, 없으면 수동으로 설치하거나 curl | sh를 써야 한다는 불만은 타당함
      다만 패키지 관리자가 uv를 저장소에 포함하기 시작하면 점점 덜 문제가 될 것임
      예를 들어 uv는 이미 Alpine Linux와 Homebrew에서 제공됨: https://repology.org/project/uv/versions
      인라인 스크립트 메타데이터는 Python 표준임
      시스템에 uv가 없고 패키징도 안 되어 있지만 스크립트에 맞는 Python 버전이 있다면 pipx로 실행할 수 있음: https://pipx.pypa.io/stable/examples/#pipx-run-examples
      pipx는 훨씬 더 널리 패키징되어 있음: https://repology.org/project/pipx/versions
    • 이런 문제를 풀려고 docker+uv shebang을 억지로 엮어 봤고 어느 정도 동작함
      임의의 개발자 머신에는 uv보다 Docker가 더 흔할 수 있고, 원문도 회사 프로젝트라고 하니 그럴듯했음
      다만 현재는 캐시를 전혀 하지 않아서 실행할 때마다 다운로드가 발생하는 점이 어색하며, 볼륨으로 고칠 수 있을 것 같음
      이런 식임: https://hugojosefson.github.io/docker-shebang/#python
    • 보통 컴퓨터에서 프로그램을 실행하려면 뭔가를 먼저 설치해야 하니 uv 설치 자체는 그리 나쁘지 않아 보임
      그래도 실행 시 인터넷에서 정체를 알 수 없는 것을 내려받는다면 self-contained라고 부르기는 어렵고, 진짜 완전한 self-contained는 AppImage에 더 가까움
    • 100% 동의함
      py2exe 같은 것을 쓰면 self-contained한 “Python 스크립트”를 만들 수 있음
      개발자에게는 문제가 많이 생기지만 사용자에게는 문제가 최소화됨
    • 사소한 보충으로, uv의 패키지 중복 제거 덕분에 고유한 의존성이 없는 한 virtualenv가 공간을 차지하지 않음
  • Nix로도 같은 방식을 쓰며 shebang 줄은 이렇게 생김
    #! nix-shell -i python3 -p "python312.withPackages (pkgs: [ pkgs.boto3 pkgs.click ])"
    이렇게 하면 시스템에 필요한 것은 Nix뿐이고, Python이 설치되어 있을 필요도 없음

    • 맞긴 하지만 아직 nixpkgs로 패키징되지 않은 PyPI 패키지가 많아서 uv만큼 범용적인 접근은 아님
    • 원문에서도 정확히 그렇듯, uv가 필요할 때 Python을 즉석에서 설치해 줌
    • 같은 기법은 어떤 언어에도 적용할 수 있음
      가장 obvious한 예는 모든 의존성을 지정한 bash이고, Nix shebang으로 빠른 단일 파일 Rust 스크립트도 만들어 본 적 있음
      https://nixos.wiki/wiki/Nix-shell_shebang
    • Nix가 설치되어 있어야 한다는 조건은 uv가 있어야 한다는 조건보다 훨씬 강한 요구사항임
    • nix-shell 대신 flake 기반 명령인 nix shell로 같은 일을 하는 방법이 궁금함
  • 다른 댓글들처럼 “self-contained”라는 주장은 uv가 설치되어 있다는 데 의존함
    정말 self-contained한 Python 스크립트를 원한다면 Nuitka 컴파일러를 살펴볼 만함
    gRPC 서비스 프로덕션에서 문제없이 쓰고 있고, nuitka --onefile run.py만 실행하면 끝남
    컴파일러이기 때문에 PyInstaller로 묶은 원래 Python 프로그램보다 결과 바이너리가 더 빠르기도 함
    작성자의 GitHub 페이지에는 “가능한 최고의 Python Compiler를 만들기 위해 늙어 죽을 때까지 시도하는 것이 내 인생의 사명”이라는 문구가 있음
    https://nuitka.net/
    https://github.com/kayhayen

  • 이 패턴은 정말 마음에 들지만, 아쉽게도 LSP와는 제대로 동작시키지 못했음
    Helix에서 pyright를 쓰고 있고, 에디터를 uv run hx script.py로 실행해도 안 됨
    uv run --with whatever-it-is-i-need hx script.py처럼 할 수는 있지만 점점 중복이 많아짐

    • 직접 만든 못생긴 uve 스크립트를 쓰고 있음
      $ cat ~/.local/bin/uve
      #!/bin/bash
      temp=$(mktemp)
      uv export --script $1 --no-hashes > $temp
      uv run --with-requirements $temp vim $1
      unlink $temp
      에디터가 곧 uv python find --script를 지원해 주면 좋겠음
  • 꽤 유용해 보임
    Python 기반 프로젝트를 장기 배포할 때 uv가 더 안전한 선택인지 궁금함
    약 5년 전 의존성 관리에 Anaconda를 썼다가, 나중에 규칙이 바뀌어서 직원 200명 이상인 조직 고객들은 더 이상 무료로 Anaconda를 쓸 수 없고 상용 라이선스를 내야 했던 rug pull을 떠올리고 있음

    • uv는 MIT 또는 Apache-2.0 라이선스임
      개발을 중단하거나 앞으로의 작업을 다른 라이선스의 포크로 옮길 수는 있지만, 과거 라이선스를 소급 변경할 수는 없으니 현재 존재하는 것은 오픈소스로 보장됨
      정말 걱정된다면 포크를 만들어 계속 동기화하면 됨
      사실상 다른 OSS 프로젝트에도 거의 동일하게 적용되는 이야기라 크게 걱정하지 않겠음
      알고 있는 한 conda는 오픈소스였던 적이 없고 바이너리를 배포해 왔음
      https://github.com/astral-sh/uv?tab=readme-ov-file#license
    • Anaconda의 rug pull은 저장소 쪽에 걸린 것으로 봄
      conda-forge의 패키지는 여전히 무료로 쓸 수 있음
      uv는 그냥 PyPI를 사용하므로, 문제가 생기면 uv에서 pip나 Poetry 등으로 바꾸면 되고 패키지는 계속 같은 곳에서 가져오게 됨
    • 프로젝트에 기여자 라이선스 계약(CLA)이 있고, 기여 저작권을 프로젝트 소유자에게 넘기게 만들면 재라이선스가 가능하다고 이해하고 있음
      그런 소유자는 결국 떠올릴 수 있는 최악의 부자에게 인수될 것임
      uv의 기여 가이드와 이슈를 훑어봤지만 CLA는 보이지 않았고, PyTorch는 기여 가이드 맨 위에 CLA가 있었음
      그래도 Anaconda의 마지막 FOSS 버전에 대한 커뮤니티 포크가 있어야 했음
      Redis에서는 그렇게 됐고, Redis도 CLA를 사용함: https://github.com/redis/redis/blob/unstable/CONTRIBUTING.md#redis-software-grant-and-contributor-license-agreement
      절대 CLA에 서명하지 말아야 함
      차라리 copyleft 프로젝트에만 기여하자고 봄
      우리는 공짜로 일하기엔 돈을 너무 많이 받음
  • 작은 유틸리티에는 컨테이너화 대신 좋은 패키징 대안처럼 보임
    이제 동료 전원에게 uv를 설치하라고 설득해야 함

    • uv는 놀라울 정도로 빠르니 도움이 될 것임
    • 우리에게는 SCA 취약점 스캐너가 아직 uv와 동작하지 않는 것이 걸림돌임
  • Ruby 쪽의 bundler/inline과 비슷해 보임
    Python에도 비슷한 게 생겨서 반갑고, 실제로 아주 편리함
    https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html

  • Windows에서 이걸 동작시킨 사람이 있는지 궁금함
    작업 중인 게임 모드용 도구에 이 트릭을 쓰고 싶었지만 shebang 트릭을 동작시키지 못했음

  • 이 사용 사례 덕분에 uv를 좋아하게 됐지만, 공식적이고 매우 유용한 PEP가 공식 Python 도구에서 지원되지 않는다는 점은 Zen of Python에 어긋난다고 느낌
    내 관점에서는 Python이 처음으로 “batteries included”가 아니었던 순간임
    이제 시스템에 Python 의존성 관리자도 두 개가 생겼음
    Python 의존성 관리에 관해 할 말이 많다는 건 알지만, 지난 몇 년 동안 프로젝트에 requirements.txt만 있으면 기본 pip+venv로 버텨 왔음

    • Python 빌드 명세에서는 그런 흐름이 좀 있었음
      pyproject.tomltomllib 라이브러리보다 먼저 나왔던 것으로 기억함
      그래서 몇 버전 동안은 Python이 기본으로 읽을 수 없는 언어로 모듈을 지정해야 했음
      쓰이지 않는 메타데이터를 포함하는 기본 방식이 있는 것보다 더 나쁜 상황임
      애초에 그래서 메타데이터인 것이고, 아니면 그냥 Python 문법이었을 것임