모듈, 패키지와 pyproject.toml
import 시스템, 모듈과 패키지의 차이, __init__.py와 __main__, 그리고 pyproject.toml로 의존성·도구 설정·배포까지 한곳에 정리합니다.
1부의 마지막 챕터입니다. 여섯 장 동안 한 파일 안에서 코드를 짰습니다. 본 챕터는 여러 파일로 쪼개고, 그것들을 프로젝트로 묶는 법 — import 시스템, 모듈과 패키지, 그리고 그 모든 걸 정의하는 pyproject.toml입니다.
본 챕터의 pyproject.toml 한 파일은 책 전체에서 계속 확장됩니다. 30장 타입체커 설정과 CI 통합에서 [tool.ruff] / [tool.pyright] 섹션을 정식 설정으로 키우고, 32장 uv로 라이브러리 배포에서 [build-system] / [project.scripts] 섹션으로 첫 라이브러리를 PyPI에 출시합니다. 본 챕터는 그 모든 작업의 토대를 깝니다.
모듈 — 파일 하나가 모듈 #
파이썬에서 .py 파일 하나가 곧 모듈입니다. 다른 곳에서 import로 가져다 씁니다.
def add(a: int, b: int) -> int:
return a + b
def multiply(a: int, b: int) -> int:
return a * b
PI = 3.141592import math_utils
print(math_utils.add(2, 3)) # 5
print(math_utils.PI) # 3.141592네 가지 import 형태 #
# 1. 모듈 전체 가져오기
import math_utils
math_utils.add(1, 2)
# 2. 별칭
import math_utils as mu
mu.add(1, 2)
# 3. 특정 이름만 가져오기
from math_utils import add, PI
add(1, 2)
# 4. 별칭 + 가져오기
from math_utils import add as plus
plus(1, 2)from m import *는 거의 안 씀
#
from math_utils import *이름 충돌, 어디서 온 이름인지 추적 불가, 정적 분석기 혼란. 새 코드에서는 거의 안 씁니다. 예외는 인터랙티브 셸에서 잠깐 써보는 경우 정도.
패키지 — 디렉터리가 패키지 #
여러 모듈을 묶고 싶을 때, 디렉터리로 만듭니다.
my_app/
├── __init__.py
├── auth.py
├── db.py
└── handlers/
├── __init__.py
├── user.py
└── post.py이 구조에서:
from my_app import auth
from my_app.db import connect
from my_app.handlers.user import create_user__init__.py — 두 가지 역할
#
__init__.py는 두 가지 일을 합니다.
- 이 디렉터리가 패키지임을 표시 (역사적 역할 — Python 3.3 이후로는 없어도 namespace package로 취급)
- 패키지가 import 될 때 실행되는 코드 — 공개 API를 정리하거나 초기화
"""my_app — 예제 애플리케이션."""
from my_app.auth import login, logout
from my_app.db import connect
__all__ = ["login", "logout", "connect"]
__version__ = "0.1.0"이러면 호출 측이:
from my_app import login, connect내부 구조(auth 모듈, db 모듈)가 노출되지 않습니다. 공개 API를 한곳에서 정의 하는 패턴입니다.
__all__은 from my_app import * 일 때 가져올 이름의 화이트리스트지만, 실제로는 공개 API를 명시하는 문서 역할로 더 자주 쓰입니다.
namespace package — __init__.py가 없으면
#
3.3+ 부터는 __init__.py 없는 디렉터리도 패키지가 됩니다(implicit namespace package). 단점:
- 초기화 코드를 둘 곳이 없음
- 일부 도구가 인식 못할 때 있음 (낡은 버전들)
새 패키지를 만들 땐 __init__.py를 두는 쪽이 안전합니다. 비어 있어도 됩니다 (pass 한 줄도 필요 없음, 빈 파일).
절대 import vs 상대 import #
같은 패키지 안에서 다른 모듈을 가져올 때 두 가지 방법이 있습니다.
# 절대 import
from my_app.db import connect
from my_app.auth import current_user
# 상대 import
from ..db import connect
from ..auth import current_user..은 “한 단계 위”, .은 “같은 디렉터리"입니다.
둘 다 됩니다만 절대 import를 권장 합니다 (PEP 8). 어디서 가져오는지 한눈에 보이고, 파일을 옮길 때 깨질 가능성도 적습니다. 상대 import는 패키지 내부 짧은 거리에서만 쓰는 정도가 무난합니다.
__main__ — 모듈을 직접 실행할 때
#
스크립트로 실행할 때만 동작하는 코드를 두는 곳입니다.
def main():
print("hello!")
if __name__ == "__main__":
main()이 파일을 두 가지 방법으로 쓸 수 있습니다.
$ uv run cli.py
hello! ← __name__ 이 '__main__'import cli
cli.main() # 명시적으로 호출하면 동작
# import 시점에는 main() 이 호출되지 않음if __name__ == "__main__":이 없으면 import만 했을 때 main이 실행되어 부수 효과가 발생합니다. 항상 이 가드를 두는 게 모범입니다.
패키지 단위 실행 — __main__.py
#
패키지 자체를 실행 가능하게 하려면:
my_cli/
├── __init__.py
├── __main__.py ← 여기 main 진입점
└── core.pyfrom my_cli.core import run
if __name__ == "__main__":
run()이러면:
$ uv run python -m my_cliCLI 도구를 만들 때 자주 쓰는 패턴입니다. 33장 CLI 도구 만들기 (Typer)에서 본격적으로 다룹니다.
표준 라이브러리 — 같은 import 시스템 #
표준 라이브러리도 똑같이 import 합니다. 별도 설치 없이 바로 씁니다.
import os # 파일 시스템
import sys # 인터프리터, argv
import json # JSON
import re # 정규식
import datetime # 날짜/시간
import pathlib # 경로 객체
from collections import Counter, defaultdict
from itertools import chain, groupby
from functools import lru_cache, partial표준 라이브러리는 공식 문서가 가장 빠른 참조입니다.
pyproject.toml — 프로젝트의 단일 정의
#
1장에서 uv init으로 만든 그 파일입니다. 본 챕터에서 본격적으로 보겠습니다.
[project]
name = "my-app"
version = "0.1.0"
description = "예제 애플리케이션"
readme = "README.md"
requires-python = ">=3.14"
authors = [
{ name = "Curtis", email = "you@example.com" },
]
license = { text = "MIT" }
dependencies = [
"httpx>=0.28",
"pydantic>=2.10",
]
[project.optional-dependencies]
docs = ["mkdocs>=1.6"]
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.7",
"pyright>=1.1",
]
[project.scripts]
my-app = "my_app.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"핵심 섹션을 하나씩.
[project] — PEP 621 메타데이터
#
이름, 버전, 설명, 파이썬 버전 요구, 작성자, 라이선스, 런타임 의존성.
[project.optional-dependencies] — 선택 의존성
#
pip install 'my-app[docs]' 같은 형태로 사용자가 선택해서 설치할 수 있는 묶음입니다. 라이브러리를 배포할 때 자주 씀.
[dependency-groups] — 개발/테스트용
#
PEP 735로 표준화된 섹션. 배포에는 안 들어가지만 개발할 땐 필요한 도구들을 묶습니다. uv가 uv add --dev로 자동 추가합니다.
[project.scripts] — CLI 엔트리포인트
#
이 섹션이 있으면 패키지 설치 시 콘솔 명령어가 자동으로 만들어집니다.
[project.scripts]
my-app = "my_app.cli:main"설치 후:
$ my-app
# my_app.cli 모듈의 main 함수가 실행됨CLI 도구를 만든다면 이 섹션이 핵심입니다. 33장 CLI 도구 만들기 (Typer)에서 실제 CLI를 만들고 본 섹션으로 엔트리포인트를 묶습니다.
[build-system] — 빌드 백엔드
#
PyPI에 올릴 패키지라면 필요. hatchling, setuptools, pdm-backend 등 여러 옵션이 있는데, 새 프로젝트는 **hatchling**이 가벼워서 무난합니다.
32장 uv로 라이브러리 배포에서 본 섹션을 정식으로 채워 PyPI 출시 과정까지 이어 갑니다.
pyproject.toml 안에 도구 설정도
#
린터 / 포매터 / 타입 체커도 같은 파일에 설정을 둡니다.
[tool.ruff]
line-length = 100
target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B"]
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.14"
[tool.pytest.ini_options]
testpaths = ["tests"]setup.cfg, .flake8, mypy.ini, pytest.ini 같은 흩어진 파일들이 하나로 모입니다. 본 챕터에서는 미리보기 수준만 보고, 정식 설정은 30장 타입체커 설정과 CI 통합에서 다룹니다.
uv 일상 명령 — 다시 한 번 #
1장에서 본 것에 더해, 자주 쓰는 흐름:
# 새 프로젝트
uv init my-app --python 3.14
# 의존성
uv add httpx pydantic
uv add --dev pytest ruff pyright
uv remove old-package
# 동기화 (다른 머신에서 / 새로 받았을 때)
uv sync
# 실행
uv run python main.py
uv run pytest
uv run ruff check
uv run pyright
# 잠깐 실행 (프로젝트 의존성이 아닌 도구)
uvx ruff check . # 한 번만 돌리고 환경 안 더럽힘
# 파이썬 자체 업그레이드
uv python install 3.14uvx는 격리된 환경에서 한 번만 실행 하는 명령입니다. pipx와 같은 역할이고, 글로벌에 도구를 설치하지 않아 안전합니다.
1부 마무리 — 여기까지 잡힌 도구 #
7장을 거쳐 모던 파이썬의 기초 인프라가 잡혔습니다. 적어도 이런 코드는 직접 쓰고 읽을 수 있습니다.
from collections.abc import Callable
type UserId = int
def find_users(
ids: list[UserId],
*,
transform: Callable[[UserId], str] = str,
skip_invalid: bool = True,
) -> dict[UserId, str]:
"""ID 리스트에서 변환된 사용자 맵을 만든다."""
result: dict[UserId, str] = {}
for uid in ids:
try:
result[uid] = transform(uid)
except ValueError:
if not skip_invalid:
raise
return result타입 힌트, | union, 빌트인 제네릭, keyword-only, Callable, 예외 처리, docstring — 모두 1부에서 다룬 도구들입니다.
연습문제 #
- 새 프로젝트
mathkit을 만들고, 패키지 구조mathkit/{__init__.py, basic.py, advanced.py, __main__.py}를 손으로 만드세요.basic.py에add,subtract,advanced.py에factorial을 두고,__init__.py에서 공개 API로 노출합니다.uv run python -m mathkit로 패키지 단위 실행이 동작하는지 확인합니다. - 위 프로젝트의
pyproject.toml에[project.scripts]로mathkit = "mathkit.__main__:main"을 추가하세요.uv pip install -e .또는uv tool install --from . mathkit로 설치 후 셸에서mathkit명령이 동작하는지 확인합니다. - 같은 프로젝트에
[dependency-groups]의 dev 그룹으로pytest,ruff,pyright를 추가한 뒤,tests/test_basic.py에add(2, 3) == 5를 검증하는 간단한 테스트를 작성하고uv run pytest가 통과하는지 확인합니다.
한 줄 요약: 파일 = 모듈, 디렉터리 = 패키지. import는 절대 경로 권장.
if __name__ == "__main__":가드는 항상, 패키지 단위 실행은__main__.py.pyproject.toml한 파일에 메타데이터 / 의존성 / 스크립트 / 도구 설정이 다 모인다. uv 일상은init / add / sync / run / uvx.
다음 챕터 #
1부가 끝나고 2부 코드 구조화가 시작됩니다. 첫 챕터는 8장 dataclass와 __slots__ — 데이터 모음 클래스를 짧고 안전하게 만드는 도구입니다. 1부의 함수 / 컬렉션 도구만으로는 장황해지던 “모양이 정해진 객체” 문제를 dataclass로 정리합니다.