파이썬 패키징 #3 pyproject.toml: 프로젝트 설정의 표준
앞 글의 결론은 “의도의 목록과 재현용 스냅샷을 분리해야 한다"였습니다. 이번 글은 그중 의도가 사는 집입니다. 요즘 파이썬 저장소를 열면 거의 반드시 루트에 pyproject.toml이 있습니다. 의존성 선언, 프로젝트 메타데이터, 포매터·린터·테스트 설정까지 전부 이 한 파일에 모입니다. 왜 이 파일이 표준이 됐고 각 구역이 무엇을 담는지 알면, 남의 프로젝트를 읽는 속도가 달라집니다.
흩어져 있던 시대에서 한 파일로 #
pyproject.toml 이전의 파이썬 프로젝트는 설정이 흩어져 있었습니다. 패키지 정보는 실행해야만 내용을 알 수 있는 setup.py 스크립트에, 의존성은 requirements.txt에, 도구 설정은 setup.cfg, .flake8, pytest.ini 등 각자의 파일에 있었습니다. PEP 518이 이 파일을 도입하고 PEP 621이 프로젝트 메타데이터의 표준 형식을 정의하면서, “프로젝트에 대한 선언은 pyproject.toml에 적는다"로 생태계가 수렴했습니다. 지금은 pip, uv, poetry 같은 설치 도구도, ruff, pytest, mypy 같은 개발 도구도 모두 이 파일을 읽습니다.
[project]: 프로젝트가 무엇인지 선언합니다 #
핵심 구역부터 보겠습니다.
[project]
name = "invoice-tool"
version = "0.1.0"
description = "청구서 PDF를 생성하는 사내 도구"
requires-python = ">=3.12"
dependencies = [
"django>=5.2,<6",
"requests~=2.32.0",
"celery>=5.5",
]- name·version: 패키지의 정체입니다. #6에서 PyPI에 배포할 때 이 이름이 그대로 쓰입니다.
- requires-python: 이 프로젝트가 도는 파이썬 버전 범위입니다. 도구들이 이 선언을 읽고 인터프리터를 맞춰 주므로 꼭 적습니다.
- dependencies: #2에서 말한 “의도의 목록"이 정확히 여기입니다. 직접 의존성만, 의도한 범위와 함께 적습니다. freeze 출력처럼 전이 의존성이 섞일 일이 없습니다.
버전을 >=5.2,<6처럼 범위로 적는 것이 요령입니다. 정확한 버전 고정은 이 파일의 일이 아니라 #5에서 다룰 lock 파일의 일입니다. 여기는 “우리 코드가 호환되는 범위"라는 의도만 선언합니다.
선택 의존성: extras와 dependency-groups #
의존성이 전부 같은 무게는 아닙니다. 두 종류의 부가 의존성을 구분해 둡니다.
[project.optional-dependencies]
pdf = ["weasyprint>=63"] # 설치하는 쪽이 고르는 기능 (extras)
[dependency-groups]
dev = [ # 개발자만 쓰는 도구 (PEP 735)
"pytest>=8",
"ruff>=0.8",
]- optional-dependencies(extras): 패키지 사용자가 기능 단위로 선택하는 의존성입니다.
pip install invoice-tool[pdf]처럼 대괄호로 골라 설치합니다. PDF 내보내기가 필요한 사람만 무거운 라이브러리를 받게 하는 구조입니다. - dependency-groups: 패키지를 쓰는 사람과 무관하게 개발하는 사람에게만 필요한 도구 목록입니다(PEP 735 표준). 테스트 시리즈에서 썼던
uv add --dev pytest가 pyproject.toml에 남기는 기록이 바로 이 구역입니다.
배포물에 포함될 수 있는 extras와 개발 환경 전용인 dependency-groups를 가르는 것이 현대 표준의 정리 방식입니다.
[build-system]: 이 프로젝트를 패키지로 만드는 방법 #
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"이 구역은 “이 프로젝트를 설치 가능한 배포물로 빌드할 때 어떤 도구를 쓰는가"의 선언입니다. 라이브러리를 만들어 배포할 때 의미를 갖고, 배포하지 않는 애플리케이션이라면 없어도 됩니다. 빌드 백엔드가 실제로 무엇을 하는지는 #6 배포 편에서 손으로 확인합니다. 지금은 “남의 프로젝트에서 이 구역을 보면 배포용 패키지구나"라고 읽을 수 있으면 충분합니다.
[tool.*]: 도구 설정의 집합소 #
pyproject.toml의 나머지 절반은 개발 도구들의 설정입니다. 도구마다 자기 이름의 네임스페이스를 씁니다.
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
strict = true.flake8, pytest.ini, mypy.ini로 흩어지던 설정이 한 파일로 모이면, 저장소 루트가 깨끗해지는 것 이상의 효과가 있습니다. 프로젝트의 규약이 한 파일에 보이므로, 새 팀원이 읽을 파일이 하나로 줄고, 설정 파일끼리 충돌하는 사고도 사라집니다.
requirements.txt와의 관계 #
정리하면 역할이 이렇게 나뉩니다. pyproject.toml의 dependencies는 선언(직접 의존성 + 호환 범위)이고, requirements.txt는 전통 워크플로에서 스냅샷까지 겸하던 파일입니다. pyproject.toml이 선언을 가져갔으므로, 남은 질문은 “스냅샷은 누가 맡는가"입니다. 그 답이 lock 파일이고, 선언에서 lock을 만들고 환경까지 맞춰 주는 도구가 다음 편의 uv입니다.
정리 #
이번 글에서 다룬 내용입니다.
- setup.py·setup.cfg·각종 도구 설정 파일로 흩어져 있던 프로젝트 정보가 PEP 518/621을 거쳐 pyproject.toml 한 파일로 수렴했습니다
[project]에는 이름, 버전, requires-python, 그리고 직접 의존성을 의도한 범위와 함께 선언합니다. 정확한 버전 고정은 lock 파일의 일입니다- 사용자가 고르는 기능은 extras(
optional-dependencies), 개발자 전용 도구는 dependency-groups(PEP 735)로 구분합니다.uv add --dev가 기록하는 곳이 후자입니다 [build-system]은 배포용 패키지를 빌드하는 방법의 선언이고, 배포하지 않는 프로젝트에는 없어도 됩니다[tool.*]에 ruff, pytest, mypy 설정을 모으면 프로젝트 규약이 한 파일에서 읽힙니다
다음 글(#4 uv)에서는 이 선언을 읽어 가상 환경, 설치, lock, 파이썬 버전까지 한 번에 처리하는 도구, uv를 다룹니다. #1〜#3에서 손으로 하던 모든 일이 명령 몇 개로 접히는 것을 보게 됩니다.