모던 파이썬 기초 #7 모듈/패키지와 pyproject.toml
여섯 편 동안 한 파일 안에서 코드를 짰습니다. 이번 마지막 글은 여러 파일로 쪼개고, 그것들을 프로젝트로 묶는 법 — import 시스템, 모듈과 패키지, 그리고 그 모든 걸 정의하는 pyproject.toml입니다.
- #1 시작과 uv 셋업
- #2 변수/기본 타입과 타입 힌트
- #3 제어 흐름
- #4 컬렉션과 컴프리헨션
- #5 함수 — 인자 패턴
- #6 에러와 예외 처리
- #7 모듈/패키지와 pyproject.toml ← 이번 글
모듈 — 파일 하나가 모듈 #
파이썬에서 .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 도구를 만들 때 자주 쓰는 패턴입니다.
표준 라이브러리 — 같은 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 도구를 만든다면 이 섹션이 핵심입니다.
[build-system] — 빌드 백엔드
#
PyPI에 올릴 패키지라면 필요. hatchling, setuptools, pdm-backend 등 여러 옵션이 있는데, 새 프로젝트는 **hatchling**이 가벼워서 무난합니다.
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 같은 흩어진 파일들이 하나로 모입니다.
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와 같은 역할이고, 글로벌에 도구를 설치하지 않아 안전합니다.
패키지를 PyPI에 배포하려면 #
여기까지 셋업이 됐다면 배포는 한두 명령입니다.
uv build # dist/ 에 wheel + sdist 생성
uvx twine upload dist/* # PyPI 업로드(twine 대신 uv publish 쓰는 흐름도 있는데, 안정성 면에서 twine이 아직 표준입니다.)
시리즈 마무리 #
이번 글에서 정리한 것:
- 파일 하나가 모듈, 디렉터리가 패키지
import의 네 가지 형태 —import x,import x as y,from x import y,from x import y as z__init__.py— 패키지 표시 + 초기화 + 공개 API 정리 (__all__)- 절대 import 권장, 상대 import는 짧은 거리에서만
if __name__ == "__main__":가드는 항상- 패키지 단위 실행은
__main__.py+python -m my_pkg pyproject.toml한 파일에 메타데이터 + 의존성 + 스크립트 + 도구 설정 모두[dependency-groups]로 개발 의존성 분리[project.scripts]로 콘솔 명령 자동 생성uv sync,uv run,uvx가 일상 워크플로우
시리즈 전체 회고 #
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 — 모두 이 시리즈에서 다룬 도구들입니다.
다음 시리즈 #
다음은 모던 파이썬 중급 시리즈입니다. 아래 주제들이 들어옵니다.
- dataclass와
__slots__ - typing 본격 — Generic, Protocol, TypedDict, Literal
- 컨텍스트 매니저 (
with,contextlib) — 이번 시리즈의finally부분에서 자주 언급된 그 도구 - 이터러블/제너레이터/
yield from - 데코레이터 패턴
- 패턴 매칭 깊이 (이번 #3
match-case의 다음 단계) - 비동기 입문 (asyncio 기초)
이 기초 시리즈가 그 위로 곧장 이어질 발판이 되도록 짠 흐름이라, 중급도 끊김 없이 들어갈 수 있을 것입니다.