목차
7 장

모듈, 패키지와 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로 가져다 씁니다.

math_utils.py
def add(a: int, b: int) -> int:
    return a + b

def multiply(a: int, b: int) -> int:
    return a * b

PI = 3.141592
main.py
import math_utils

print(math_utils.add(2, 3))     # 5
print(math_utils.PI)            # 3.141592

네 가지 import 형태 #

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

이 구조에서:

패키지 import
from my_app import auth
from my_app.db import connect
from my_app.handlers.user import create_user

__init__.py — 두 가지 역할 #

__init__.py는 두 가지 일을 합니다.

  1. 이 디렉터리가 패키지임을 표시 (역사적 역할 — Python 3.3 이후로는 없어도 namespace package로 취급)
  2. 패키지가 import 될 때 실행되는 코드 — 공개 API를 정리하거나 초기화
my_app/__init__.py
"""my_app — 예제 애플리케이션."""

from my_app.auth import login, logout
from my_app.db import connect

__all__ = ["login", "logout", "connect"]
__version__ = "0.1.0"

이러면 호출 측이:

짧아진 import
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 #

같은 패키지 안에서 다른 모듈을 가져올 때 두 가지 방법이 있습니다.

my_app/handlers/user.py
# 절대 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__ — 모듈을 직접 실행할 때 #

스크립트로 실행할 때만 동작하는 코드를 두는 곳입니다.

cli.py
def main():
    print("hello!")

if __name__ == "__main__":
    main()

이 파일을 두 가지 방법으로 쓸 수 있습니다.

직접 실행
$ uv run cli.py
hello!         ← __name__ 이 '__main__'
다른 곳에서 import
import cli
cli.main()    # 명시적으로 호출하면 동작
# import 시점에는 main() 이 호출되지 않음

if __name__ == "__main__":이 없으면 import만 했을 때 main이 실행되어 부수 효과가 발생합니다. 항상 이 가드를 두는 게 모범입니다.

패키지 단위 실행 — __main__.py #

패키지 자체를 실행 가능하게 하려면:

실행 가능한 패키지
my_cli/
├── __init__.py
├── __main__.py     ← 여기 main 진입점
└── core.py
my_cli/__main__.py
from my_cli.core import run

if __name__ == "__main__":
    run()

이러면:

패키지로 실행
$ uv run python -m my_cli

CLI 도구를 만들 때 자주 쓰는 패턴입니다. 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으로 만든 그 파일입니다. 본 챕터에서 본격적으로 보겠습니다.

pyproject.toml
[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.14

uvx격리된 환경에서 한 번만 실행 하는 명령입니다. 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부에서 다룬 도구들입니다.

연습문제 #

  1. 새 프로젝트 mathkit을 만들고, 패키지 구조 mathkit/{__init__.py, basic.py, advanced.py, __main__.py}를 손으로 만드세요. basic.pyadd, subtract, advanced.pyfactorial을 두고, __init__.py에서 공개 API로 노출합니다. uv run python -m mathkit로 패키지 단위 실행이 동작하는지 확인합니다.
  2. 위 프로젝트의 pyproject.toml[project.scripts]mathkit = "mathkit.__main__:main"을 추가하세요. uv pip install -e . 또는 uv tool install --from . mathkit로 설치 후 셸에서 mathkit 명령이 동작하는지 확인합니다.
  3. 같은 프로젝트에 [dependency-groups]의 dev 그룹으로 pytest, ruff, pyright를 추가한 뒤, tests/test_basic.pyadd(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로 정리합니다.

X