모던 파이썬 기초 #7 모듈/패키지와 pyproject.toml

6 분 소요

여섯 편 동안 한 파일 안에서 코드를 짰습니다. 이번 마지막 글은 여러 파일로 쪼개고, 그것들을 프로젝트로 묶는 법import 시스템, 모듈과 패키지, 그리고 그 모든 걸 정의하는 pyproject.toml입니다.

  • #1 시작과 uv 셋업
  • #2 변수/기본 타입과 타입 힌트
  • #3 제어 흐름
  • #4 컬렉션과 컴프리헨션
  • #5 함수 — 인자 패턴
  • #6 에러와 예외 처리
  • #7 모듈/패키지와 pyproject.toml ← 이번 글

모듈 — 파일 하나가 모듈 #

파이썬에서 .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 도구를 만들 때 자주 쓰는 패턴입니다.

표준 라이브러리 — 같은 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 도구를 만든다면 이 섹션이 핵심입니다.

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

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

다음 시리즈 #

다음은 모던 파이썬 중급 시리즈입니다. 아래 주제들이 들어옵니다.

  1. dataclass와 __slots__
  2. typing 본격 — Generic, Protocol, TypedDict, Literal
  3. 컨텍스트 매니저 (with, contextlib) — 이번 시리즈의 finally 부분에서 자주 언급된 그 도구
  4. 이터러블/제너레이터/yield from
  5. 데코레이터 패턴
  6. 패턴 매칭 깊이 (이번 #3 match-case의 다음 단계)
  7. 비동기 입문 (asyncio 기초)

이 기초 시리즈가 그 위로 곧장 이어질 발판이 되도록 짠 흐름이라, 중급도 끊김 없이 들어갈 수 있을 것입니다.

X