Virtual environment, pip 및 pyproject.toml Python
1. 개요
한 대의 머신에서 여러 Python 프로젝트를 진행하면, 곧 버전 충돌을 마주칠 것입니다. 프로젝트 A는 requests 2.28이 필요하고, 프로젝트 B는 requests 2.32가 필요합니다. 이 글에서는 그 문제를 해결하는 세 가지 도구를 설명합니다. venv는 각 프로젝트마다 환경을 격리합니다. pip은 패키지를 설치하고 관리합니다. pyproject.toml은 현대적인 표준에 따라 프로젝트를 선언합니다. 이 글을 읽고 나면, 표준 구조의 Python 프로젝트를 구축할 수 있고, editable 모드로 설치할 수 있으며, pytest로 테스트를 실행할 수 있게 됩니다.
2. Virtual environment
Virtual environment (venv)는 세 가지를 포함하는 디렉토리입니다: 독립적인 Python interpreter 사본(보통 symlink), 독립적인 site-packages 디렉토리, 그리고 활성화 스크립트. venv에 설치한 패키지는 해당 디렉토리에만 있으며, 시스템 Python에 영향을 주지 않습니다.
2.1. venv 생성 및 활성화
venv 모듈은 Python 3.3부터 표준 라이브러리에 포함되어 있으므로 추가 설치가 필요 없습니다:
mkdir shopcart && cd shopcart
python3 -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
python3 -m venv .venv 명령은 프로젝트 내에 .venv 디렉토리를 생성합니다. .venv라는 이름은 일반적인 관례이며, 대부분의 IDE가 자동으로 인식합니다. 활성화한 후, 셸 프롬프트는 (.venv) 접두사를 갖게 됩니다. 이때 python과 pip 명령은 venv를 가리킵니다.
venv를 종료하려면 다음을 실행하세요:
deactivate
2.2. 현재 사용 중인 interpreter 확인
많은 사람들이 이 단계를 건너뛰며, 이것이 ’설치했는데도 ModuleNotFoundError가 나온다’는 종류의 오류의 원인입니다:
which python
python -c "import sys; print(sys.prefix); print(sys.prefix != sys.base_prefix)"
/home/lan/shopcart/.venv/bin/python
/home/lan/shopcart/.venv
True
sys.prefix는 현재 실행 중인 환경의 루트 디렉토리입니다. sys.base_prefix는 기본 Python 설치 디렉토리입니다. 두 값이 다르면 venv 내에 있다는 의미입니다.
참고:
.venv디렉토리를 Git에 커밋하지 마세요..venv/를.gitignore에 추가하세요. venv는 절대 경로와 운영 체제에 의존하므로 다른 머신에 복사하면 대부분 작동하지 않습니다. 올바른 방법은 의존성 선언 파일을 커밋한 후 새 머신에서 venv를 다시 생성하는 것입니다.
3. pip으로 패키지 관리
pip은 기본 패키지 설치 프로그램이며, PyPI에서 패키지를 가져옵니다.
3.1. 기본 명령
python -m pip install --upgrade pip
python -m pip install "requests>=2.31,<3"
python -m pip list
python -m pip show requests
python -m pip uninstall -y requests
각 명령은 다음을 수행합니다:
install --upgrade pip는 pip 자체를 업그레이드합니다. 오래된 pip 버전은 때때로pyproject.toml을 올바르게 읽지 못합니다."requests>=2.31,<3"은 버전 지정자입니다. 셸이<를 리다이렉션으로 해석하지 않도록 따옴표로 감싸야 합니다.list는 설치된 패키지를 나열합니다.show는 버전, 설치 위치, 그것이 의존하는 패키지 등 자세한 정보를 출력합니다.
3.2. pip 대신 python -m pip를 사용해야 하는 이유
단순히 pip 명령은 현재 사용 중인 python과 다른 Python을 가리킬 수 있습니다. 이는 머신에 여러 버전의 Python이 있거나 venv 활성화를 잊었을 때 자주 발생합니다. python -m pip 명령은 항상 올바른 interpreter의 pip를 실행하므로 패키지가 원하는 환경에 설치됩니다.
3.3. requirements.txt와 pip freeze
패키지 목록을 공유하는 전통적인 방법은 requirements.txt를 사용하는 것입니다:
python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3
pip freeze는 venv의 모든 패키지를 urllib3과 같은 간접 의존성을 포함해 정확한 버전으로 기록합니다. 이 방법은 실행 환경을 재현하는 데 적합합니다. 그러나 어떤 패키지가 실제로 필요하고 어떤 패키지가 단순히 끌려온 것인지 알 수 없습니다. 또한 프로젝트를 설명하지 않습니다: 이름, 버전, Python 요구 사항. 그것이 pyproject.toml의 역할입니다.
4. pyproject.toml으로 프로젝트 선언
pyproject.toml은 Python 프로젝트의 표준 구성 파일입니다. 이는 PEP 518, PEP 621 및 관련 PEP에서 정의됩니다. 이 파일은 setup.py, setup.cfg를 대체하며 pytest나 ruff 같은 많은 도구의 구성을 한 곳에 모읍니다.
4.1. 프로젝트 구조
우리는 src layout을 사용합니다. 즉, src/ 내에 코드를 배치합니다. 이 레이아웃은 테스트가 항상 설치된 패키지에서 실행되도록 하며, 실수로 작업 디렉토리에서 직접 가져오지 않습니다:
shopcart/
├── pyproject.toml
├── src/
│ └── shopcart/
│ ├── __init__.py
│ ├── order.py
│ └── cli.py
└── tests/
└── test_order.py
4.2. 완전한 pyproject.toml 파일
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "shopcart"
version = "0.1.0"
description = "간단한 주문 가격 계산"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31,<3",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.5",
]
[project.scripts]
shopcart = "shopcart.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
각 섹션의 의미:
[build-system]은 pip에 프로젝트를 빌드하는 데 필요한 도구를 알려줍니다. 여기서는 setuptools입니다. 나머지를 수정하지 않고hatchling또는flit_core로 바꿀 수 있습니다.[project]는 표준 메타데이터를 포함합니다.requires-python은 지원되지 않는 Python 버전에 프로젝트를 설치하는 것을 방지합니다.dependencies는 프로젝트를 실행하는 데 필수 패키지입니다. 여기서는 버전 범위(>=2.31,<3)를 선언해야 하며, 고정(==2.32.3)하지 말아야 합니다.[project.optional-dependencies]는 선택적 패키지를 그룹화합니다.dev그룹은 최종 사용자가 필요하지 않은 개발 도구를 포함합니다.[project.scripts]는 터미널에서shopcart명령을 생성합니다. 이 명령은shopcart.cli모듈의main함수를 호출합니다.[tool.pytest.ini_options]는 pytest의 전용 구성입니다. 모든 도구는 자신의[tool.<name>]네임스페이스를 가집니다.
src layout을 사용하면 setuptools는 자동으로 src/에서 패키지를 찾으므로 추가 선언이 필요 없습니다.
4.3. 패키지 코드
# src/shopcart/order.py
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
product: str
unit_price: Decimal
quantity: int
@dataclass
class Order:
customer: str
lines: list[OrderLine] = field(default_factory=list)
def add(self, product: str, unit_price: str, quantity: int = 1) -> None:
if quantity <= 0:
raise ValueError("quantity는 0보다 커야 합니다")
self.lines.append(OrderLine(product, Decimal(unit_price), quantity))
def total(self) -> Decimal:
return sum((l.unit_price * l.quantity for l in self.lines), Decimal("0"))
Order는 OrderLine의 목록을 보유합니다. 가격은 부동소수점 오류를 피하기 위해 Decimal을 사용하므로 unit_price는 문자열로 전달됩니다. total() 메서드는 각 줄의 소계를 더하며, 초기값은 Decimal("0")입니다.
# src/shopcart/cli.py
from shopcart.order import Order
def main() -> None:
order = Order(customer="Nguyễn Văn An")
order.add("커피", "29000", 2)
order.add("베트남 바게트", "25000")
print(f"고객: {order.customer}")
print(f"총액: {order.total():,} VND")
src/shopcart/__init__.py 파일은 비워둘 수 있습니다.
4.4. Editable 모드로 설치
python -m pip install -e ".[dev]"
shopcart
고객: Nguyễn Văn An
총액: 83,000 VND
-e (editable) 플래그는 패키지를 site-packages에 복사하는 대신 소스 코드에 링크합니다. 따라서 order.py를 수정하면 다시 설치할 필요 없이 변경 사항이 즉시 적용됩니다. ".[dev]"는 현재 디렉토리의 프로젝트를 dev 그룹과 함께 설치하므로 pytest와 ruff도 설치됩니다. [project.scripts] 선언 덕분에 shopcart 명령이 터미널에서 사용 가능합니다.
참고: zsh 같은 일부 셸은
[...]를 글로브 패턴으로 해석합니다. 따라서 항상".[dev]"를 따옴표로 감싸야 합니다.
5. 테스트 작성 및 실행
# tests/test_order.py
from decimal import Decimal
import pytest
from shopcart.order import Order
def test_total_sums_all_lines():
order = Order(customer="Trần Thị Bình")
order.add("복숭아 차", "35000", 2)
order.add("커스터드 크림", "15000", 3)
assert order.total() == Decimal("115000")
def test_empty_order_has_zero_total():
assert Order(customer="Lê Minh").total() == Decimal("0")
def test_add_rejects_non_positive_quantity():
order = Order(customer="Phạm Hà")
with pytest.raises(ValueError, match="quantity"):
order.add("생수", "10000", 0)
python -m pytest -q
... [100%]
3 passed in 0.02s
테스트는 shopcart.order을 설치된 패키지로 가져옵니다. 이것이 위의 pip install -e 단계가 필요한 이유입니다. 설치하지 않으면 src layout은 ModuleNotFoundError를 보고하며, 이것은 의도적입니다: 사용자가 설치할 것을 올바르게 테스트하도록 강제합니다. 세 번째 테스트는 pytest.raises를 사용하여 유효하지 않은 수량이 “quantity”라는 단어를 포함하는 메시지와 함께 거부되는지 확인합니다.
6. 버전 고정: pyproject.toml 또는 requirements.txt?
이 두 파일은 상호 배타적이지 않습니다. 서로 다른 목적을 제공합니다:
pyproject.toml |
Lock 파일 (requirements.txt 고정) |
|
|---|---|---|
| 목적 | 프로젝트에 필요한 것 설명 | 환경을 정확하게 재현 |
| 버전 | 범위 (>=2.31,<3) |
고정 (==2.32.3) |
| 사용처 | 라이브러리 및 애플리케이션 | 배포되는 애플리케이션 (서버, Docker) |
PyPI에 게시할 라이브러리의 경우 pyproject.toml만 필요합니다. 버전을 고정하면 라이브러리 사용자가 충돌을 경험할 것입니다.
서버에서 실행되는 애플리케이션의 경우, 모든 배포가 동일한 버전 집합을 설치하도록 lock 파일을 추가해야 합니다:
python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock
배포할 때는 먼저 lock 파일에서 설치한 후 프로젝트를 설치하여 pip이 의존성 버전을 자동으로 업그레이드하지 않도록 합니다:
python -m pip install -r requirements.lock
python -m pip install --no-deps .
lock 프로세스를 더 간단하게 하려면 pip-tools (명령 pip-compile pyproject.toml) 또는 uv를 사용할 수 있습니다. 둘 다 pyproject.toml을 직접 읽고 해시가 포함된 lock 파일을 생성합니다.
7. 흔한 오류
sudo pip install로 패키지를 설치합니다. 이 명령은 운영 체제 패키지를 덮어쓰고 시스템 도구를 손상시킬 수 있습니다. 많은 최신 Linux 배포판은externally-managed-environment오류로 완전히 차단합니다. 해결책은 항상 venv를 사용하는 것입니다.- 설치 또는 실행 전에 venv를 활성화하는 것을 잊습니다. 섹션 2.2에서와 같이
which python으로 확인하거나.venv/bin/python -m pip ...을 직접 호출할 수 있습니다. - 한 Python으로 venv를 생성하고 다른 Python으로 실행합니다. venv는 그것을 만든 interpreter에 밀접하게 연결됩니다. Python을 업그레이드할 때(예: 3.11에서 3.12로)
.venv를 삭제하고 다시 생성하세요. - 프로젝트 디렉토리를 이동하거나 이름을 바꿉니다.
.venv/bin의 스크립트는 절대 경로를 포함하므로 이동 후 작동하지 않습니다. venv를 다시 생성해야 합니다. - 오래된
setup.py와pyproject.toml을 혼합하고 둘 다에서 의존성을 선언합니다. 하나의 진실 공급원을 선택하세요. 새 프로젝트의 경우[project]섹션입니다. - 개발 도구를
dependencies에 포함합니다.pytest또는ruff는 최종 사용자가 설치할 필요가 없도록optional-dependencies(또는 도구가 지원하는 경우[dependency-groups])에 있어야 합니다.
참고:
black또는httpie와 같은 독립적인 CLI 도구를 머신 전체에서 실행해야 하는 경우에만 venv를 사용하지 않아야 합니다. 이 경우pipx가 더 적합합니다: 각 도구마다 자체 venv를 생성하고 명령을PATH에 추가합니다.
8. 결론
이 글은 Python 프로젝트의 세 가지 기본 도구를 다루었습니다:
python -m venv .venv는 각 프로젝트마다 의존성을 격리합니다.python -m pip는 현재 사용 중인 interpreter에 패키지를 설치합니다.pyproject.toml은 메타데이터, 의존성, 선택적 그룹, CLI 명령 및 도구 구성을 하나의 파일로 선언합니다.
src layout, pip install -e ".[dev]"와 pytest를 결합하면 깨끗하고, 테스트하기 쉽고, 공유하기 쉬운 프로젝트를 갖게 됩니다. 안정적으로 배포해야 하는 애플리케이션의 경우 pyproject.toml 옆에 lock 파일을 추가하여 모든 환경이 동일한 버전 집합을 설치하도록 하세요.