← Back to Blog

[Python] Managing Packages with __init__.py

computer science > programming

2026-07-064 min read

#computer-science #programming #python #package #module #init-py

init.py란?

__init__.py는 Python에서 directory를 package로 다룰 때 사용하는 파일이다.

예를 들어 다음과 같은 구조가 있다고 하자.

my_app/
  main.py
  users/
    __init__.py
    models.py
    services.py

users directory 안에 __init__.py가 있으면 Python은 users를 package로 인식한다. 그래서 다음처럼 import할 수 있다.

from users.models import User
from users.services import create_user

최근 Python에서는 __init__.py가 없어도 namespace package로 동작할 수 있다. 하지만 일반적인 application code에서는 package 의도를 명확히 하기 위해 __init__.py를 두는 경우가 많다.


module과 package

먼저 용어를 정리하자.

용어의미
module하나의 .py 파일
package여러 module을 담는 directory
__init__.pypackage가 import될 때 실행되는 초기화 파일

예를 들어 다음 구조에서 models.py는 module이고, users는 package이다.

users/
  __init__.py
  models.py
  services.py

import 기준으로 보면 다음과 같다.

import users
import users.models

import users를 실행하면 users/__init__.py가 실행된다. import users.models를 실행하면 package 초기화 후 models.py가 import된다.


init.py

가장 단순한 __init__.py는 빈 파일이다.

users/
  __init__.py
  models.py
  services.py

이 경우 __init__.py는 별도 코드를 실행하지 않고, 단지 이 directory가 package라는 것을 명확히 표시한다.

처음에는 빈 파일로 시작해도 충분하다. package가 커지고 public API를 정리해야 할 때 내용을 추가하면 된다.


package public API 만들기

__init__.py는 package의 public API를 모아두는 용도로 자주 사용된다.

예를 들어 users/models.py가 다음과 같다고 하자.

from dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str

그리고 users/services.py가 다음과 같다고 하자.

from users.models import User


def create_user(id: int, name: str) -> User:
    return User(id=id, name=name)

이제 users/__init__.py에서 필요한 이름을 다시 export할 수 있다.

from users.models import User
from users.services import create_user

그러면 사용하는 쪽에서는 더 짧게 import할 수 있다.

from users import User, create_user

이렇게 하면 package 내부 파일 구조가 바뀌어도 외부 import 경로를 안정적으로 유지하기 쉽다.


all

__all__from package import *를 했을 때 외부로 노출할 이름을 정한다.

from users.models import User
from users.services import create_user

__all__ = ["User", "create_user"]

이제 다음 import에서 User, create_user만 import 대상이 된다.

from users import *

import *는 일반 application code에서 많이 권장되지는 않는다. 하지만 library를 만들 때 public API를 문서화하는 용도로 __all__을 두면 도움이 된다.

__all__은 다음 의미를 가진다.

이 package에서 외부 사용자가 써도 되는 이름 목록

내부 module 숨기기

package 내부 구현은 _ prefix로 표시할 수 있다.

users/
  __init__.py
  models.py
  services.py
  _validators.py

_validators.py는 내부용 module이라는 의미이다. Python이 강제로 import를 막지는 않지만, 외부에서 직접 사용하지 말라는 관례이다.

# users/_validators.py
def validate_name(name: str) -> None:
    if not name:
        raise ValueError("name is required")

외부에는 create_user만 노출한다.

# users/__init__.py
from users.models import User
from users.services import create_user

__all__ = ["User", "create_user"]

이렇게 하면 package를 사용하는 쪽에서는 내부 구현보다 public API를 중심으로 사용하게 된다.


import 경로 단순화

__init__.py를 잘 쓰면 import 경로를 단순화할 수 있다.

다음처럼 내부 구조가 깊다고 하자.

my_app/
  auth/
    __init__.py
    domain/
      __init__.py
      user.py
      token.py
    use_cases/
      __init__.py
      login.py

그냥 import하면 경로가 길어진다.

from auth.domain.user import User
from auth.use_cases.login import login

auth/__init__.py에서 필요한 이름을 re-export하면 더 단순하게 만들 수 있다.

from auth.domain.user import User
from auth.use_cases.login import login

__all__ = ["User", "login"]

사용하는 쪽에서는 다음처럼 쓸 수 있다.

from auth import User, login

다만 너무 많은 이름을 최상위 package에 몰아넣으면 오히려 어디에서 온 이름인지 알기 어려워진다. 자주 쓰는 핵심 API만 올리는 것이 좋다.


package 초기화 코드

__init__.py는 package가 import될 때 실행된다. 그래서 초기화 코드를 넣을 수 있다.

print("users package loaded")

하지만 일반적으로 __init__.py에 무거운 실행 코드를 넣는 것은 좋지 않다.

예를 들어 다음 작업은 피하는 것이 좋다.

DB connection 생성
network request 실행
큰 파일 읽기
환경 변수 검증 후 process 종료
오래 걸리는 초기화

이유는 단순하다. import users만 했는데 예상하지 못한 작업이 실행될 수 있기 때문이다.

좋은 기준은 다음과 같다.

__init__.py에는 import와 가벼운 상수 정의 정도만 둔다.
실제 초기화는 명시적인 함수로 분리한다.

예를 들어 database 초기화는 다음처럼 명시적으로 호출하는 편이 낫다.

def init_database():
    ...

circular import 주의

__init__.py에서 여러 module을 한꺼번에 import하면 circular import가 생길 수 있다.

예를 들어 다음 구조를 보자.

users/
  __init__.py
  models.py
  services.py

__init__.py에서 Usercreate_user를 export한다.

from users.models import User
from users.services import create_user

그런데 services.py에서 다시 package root를 import하면 문제가 생길 수 있다.

# users/services.py
from users import User  # circular import 위험

이 경우 내부 module끼리는 직접 module 경로를 import하는 편이 안전하다.

# users/services.py
from users.models import User

기준은 다음과 같다.

package 외부에서는 from users import User
package 내부에서는 from users.models import User

이렇게 나누면 circular import 가능성을 줄일 수 있다.


relative import

package 내부에서는 relative import를 사용할 수도 있다.

# users/services.py
from .models import User
from ._validators import validate_name

.은 현재 package를 의미한다. ..은 상위 package를 의미한다.

from ..database import Session

relative import는 package 내부 구조를 기준으로 import하므로, package 이동 시 수정량이 줄어들 수 있다. 반면 너무 많이 사용하면 import 경로를 읽기 어려워질 수 있다.

작은 package에서는 relative import가 깔끔하다. 큰 application에서는 absolute import를 선호하는 팀도 많다.


version 같은 metadata

library package에서는 __init__.py에 version 같은 metadata를 두기도 한다.

__version__ = "0.1.0"

사용자는 다음처럼 확인할 수 있다.

import my_library

print(my_library.__version__)

다만 package metadata를 pyproject.toml에서 관리하는 경우에는 중복 관리 문제가 생길 수 있다. 작은 library에서는 괜찮지만, 큰 project에서는 version 관리 방식을 하나로 정하는 것이 좋다.


예시 구조

다음은 __init__.py를 사용해 package public API를 정리한 예시이다.

shop/
  __init__.py
  products.py
  orders.py
  _validators.py

products.py

from dataclasses import dataclass


@dataclass
class Product:
    id: int
    name: str
    price: int

orders.py

from shop.products import Product
from shop._validators import validate_quantity


def create_order(product: Product, quantity: int) -> dict:
    validate_quantity(quantity)

    return {
        "product_id": product.id,
        "quantity": quantity,
        "total_price": product.price * quantity,
    }

_validators.py

def validate_quantity(quantity: int) -> None:
    if quantity <= 0:
        raise ValueError("quantity must be positive")

__init__.py

from shop.orders import create_order
from shop.products import Product

__all__ = ["Product", "create_order"]

사용하는 쪽에서는 다음처럼 쓴다.

from shop import Product, create_order

product = Product(id=1, name="Keyboard", price=100)
order = create_order(product, quantity=2)

외부 사용자는 shop.products, shop.orders의 위치를 몰라도 된다. package root에서 공개한 API만 사용하면 된다.


언제 비워두고 언제 작성할까

__init__.py는 항상 복잡하게 작성할 필요가 없다.

상황권장
단순 package 표시__init__.py
public API 정리핵심 class/function re-export
library 개발__all__, __version__ 고려
큰 applicationimport side effect 최소화
circular import가 잦음__init__.py import 줄이기

처음에는 빈 파일로 시작하고, 외부에서 자주 import하는 이름이 명확해질 때 re-export를 추가하는 방식이 안전하다.


정리

__init__.py는 Python package를 관리하는 중요한 파일이다.

핵심은 다음과 같다.

  1. directory를 package로 명확히 표시한다.
  2. package가 import될 때 실행된다.
  3. public API를 re-export할 수 있다.
  4. __all__로 공개할 이름을 정리할 수 있다.
  5. 내부 module은 _module.py처럼 표시할 수 있다.
  6. 무거운 초기화 코드는 넣지 않는 것이 좋다.
  7. package 내부에서는 circular import를 조심해야 한다.

간단히 말하면 __init__.py는 package의 입구이다. 외부 사용자가 어떤 이름을 import해야 하는지 정리하고, 내부 파일 구조를 숨기는 역할을 한다.