Skip to main content

타입 힌트

학습 목표

  • 기본 타입 어노테이션을 함수와 변수에 적용할 수 있다
  • Optional, Union, Literal 등 고급 타입을 활용할 수 있다
  • GenericTypeVar로 재사용 가능한 타입을 정의할 수 있다
  • TYPE_CHECKING으로 순환 참조를 해결할 수 있다

왜 중요한가

타입 힌트(Type Hint)는 Python 3.5부터 도입된 정적 타입 표기 시스템입니다. ML/DL 프로젝트가 커질수록 함수의 입출력 타입이 불분명해지기 쉽습니다. 타입 힌트는 코드 문서화, IDE 자동완성, 정적 분석(mypy)을 통해 버그를 사전에 방지합니다. FastAPI, Pydantic 등 현대 Python 프레임워크는 타입 힌트를 핵심 기능으로 활용합니다.

기본 타입 어노테이션

1

변수와 함수의 타입 표기

# 변수 타입 어노테이션
name: str = "GPT"
learning_rate: float = 0.001
epochs: int = 50
is_training: bool = True

# 함수 타입 어노테이션
def greet(name: str) -> str:
    return f"안녕하세요, {name}님!"

# 반환값이 없는 함수
def log_message(message: str) -> None:
    print(f"[LOG] {message}")

# 여러 매개변수
def train(model: str, lr: float, epochs: int) -> float:
    """학습을 수행하고 최종 정확도를 반환합니다."""
    print(f"{model} 학습: lr={lr}, epochs={epochs}")
    return 0.95
2

컬렉션 타입

# Python 3.9+ : 내장 타입을 직접 사용
scores: list[float] = [0.9, 0.85, 0.92]
config: dict[str, int] = {"epochs": 50, "batch_size": 32}
unique_labels: set[str] = {"cat", "dog", "bird"}
point: tuple[float, float] = (1.0, 2.0)

# 가변 길이 튜플
values: tuple[int, ...] = (1, 2, 3, 4, 5)

# 중첩 컬렉션
matrix: list[list[float]] = [[1.0, 0.0], [0.0, 1.0]]
experiments: dict[str, list[float]] = {
    "accuracy": [0.8, 0.85, 0.9],
    "loss": [0.5, 0.3, 0.1],
}

# Python 3.8 이하에서는 typing 모듈 사용
from typing import List, Dict, Set, Tuple
scores_old: List[float] = [0.9, 0.85]
Python 3.9 이상에서는 list[int], dict[str, float]처럼 소문자 내장 타입을 직접 사용할 수 있습니다. typing.List, typing.Dict는 더 이상 필요하지 않습니다.
3

함수 매개변수 패턴

from collections.abc import Callable, Sequence, Iterable

# Callable - 함수를 매개변수로 받을 때
def apply_transform(
    data: list[float],
    transform: Callable[[float], float]
) -> list[float]:
    return [transform(x) for x in data]

# 사용
normalized = apply_transform([1, 2, 3], lambda x: x / 3)

# Sequence - 리스트, 튜플 등 순서형 컬렉션
def mean(values: Sequence[float]) -> float:
    return sum(values) / len(values)

# Iterable - 순회 가능한 객체
def process_all(items: Iterable[str]) -> list[str]:
    return [item.upper() for item in items]

Optional과 Union

from typing import Optional, Union

# Optional[X] = X | None (Python 3.10+)
def find_user(user_id: int) -> Optional[str]:
    """사용자를 찾으면 이름을, 못 찾으면 None을 반환합니다."""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# Python 3.10+ 문법: X | None
def find_user_new(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# Union - 여러 타입 중 하나
def process_input(data: Union[str, list[str]]) -> list[str]:
    """문자열 또는 문자열 리스트를 받아 리스트로 통일합니다."""
    if isinstance(data, str):
        return [data]
    return data

# Python 3.10+ 문법
def process_input_new(data: str | list[str]) -> list[str]:
    if isinstance(data, str):
        return [data]
    return data
Optional[str]은 “선택적 매개변수”가 아니라 “str 또는 None”을 의미합니다. 선택적 매개변수는 기본값으로 표현합니다: def f(x: str = "default").

Literal과 TypedDict

from typing import Literal, TypedDict

# Literal - 허용되는 값을 제한
def set_device(device: Literal["cpu", "cuda", "mps"]) -> None:
    print(f"장치 설정: {device}")

set_device("cuda")   # OK
# set_device("tpu")  # mypy 에러: "tpu"는 허용되지 않음

# Literal로 모드 제한
def split_data(
    mode: Literal["train", "val", "test"],
    ratio: float = 0.8,
) -> None:
    print(f"모드: {mode}, 비율: {ratio}")

# TypedDict - 딕셔너리의 키별 타입 지정
class ModelConfig(TypedDict):
    name: str
    hidden_size: int
    num_layers: int
    dropout: float

config: ModelConfig = {
    "name": "bert-base",
    "hidden_size": 768,
    "num_layers": 12,
    "dropout": 0.1,
}

# total=False로 선택적 키 허용
class TrainingConfig(TypedDict, total=False):
    epochs: int
    learning_rate: float
    warmup_steps: int  # 선택적

partial_config: TrainingConfig = {"epochs": 10}  # OK

Generic과 TypeVar

from typing import TypeVar, Generic

# TypeVar - 타입 변수 정의
T = TypeVar("T")

def first_element(items: list[T]) -> T:
    """리스트의 첫 번째 요소를 반환합니다."""
    return items[0]

# 타입이 자동으로 추론됨
name = first_element(["Alice", "Bob"])     # str로 추론
score = first_element([0.9, 0.8, 0.7])    # float로 추론

# 타입 범위 제한
Number = TypeVar("Number", int, float)

def add(a: Number, b: Number) -> Number:
    return a + b

# Generic 클래스 - 타입 매개변수를 가진 클래스
class DataLoader(Generic[T]):
    """범용 데이터 로더"""

    def __init__(self, data: list[T], batch_size: int = 32) -> None:
        self.data = data
        self.batch_size = batch_size

    def get_batch(self, index: int) -> list[T]:
        start = index * self.batch_size
        end = start + self.batch_size
        return self.data[start:end]

    def __len__(self) -> int:
        return (len(self.data) + self.batch_size - 1) // self.batch_size

# 사용
str_loader = DataLoader[str](["a", "b", "c"], batch_size=2)
int_loader = DataLoader[int]([1, 2, 3, 4, 5], batch_size=2)

TYPE_CHECKING과 순환 참조 해결

from __future__ import annotations  # 모든 어노테이션을 문자열로 지연 평가
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # 타입 검사 시에만 import (런타임에는 실행되지 않음)
    from torch import Tensor
    from model import MyModel

class Trainer:
    def __init__(self, model: "MyModel", lr: float = 0.001) -> None:
        self.model = model
        self.lr = lr

    def train_step(self, batch: "Tensor") -> float:
        """한 스텝 학습을 수행합니다."""
        loss = 0.0  # 실제 학습 로직
        return loss
from __future__ import annotations를 파일 최상단에 추가하면 모든 타입 어노테이션이 문자열로 처리되어, 아직 정의되지 않은 클래스도 참조할 수 있습니다.

타입 가드와 타입 좁히기

from typing import TypeGuard

# isinstance로 타입 좁히기
def process(value: int | str) -> str:
    if isinstance(value, int):
        # 여기서 value는 int로 좁혀짐
        return str(value * 2)
    # 여기서 value는 str로 좁혀짐
    return value.upper()

# TypeGuard - 커스텀 타입 가드 (Python 3.10+)
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    """리스트의 모든 요소가 문자열인지 확인합니다."""
    return all(isinstance(x, str) for x in val)

def process_items(items: list[object]) -> None:
    if is_string_list(items):
        # 여기서 items는 list[str]로 좁혀짐
        for item in items:
            print(item.upper())  # str 메서드 사용 가능

AI/ML에서의 활용

from __future__ import annotations
from typing import TypeVar, Generic, Literal, TypedDict
from collections.abc import Callable, Sequence

# 학습 설정 타입 정의
class HyperParams(TypedDict):
    learning_rate: float
    batch_size: int
    epochs: int
    optimizer: Literal["adam", "sgd", "adamw"]

# 메트릭 콜백 타입
MetricFn = Callable[[list[float], list[float]], float]

def evaluate(
    y_true: Sequence[float],
    y_pred: Sequence[float],
    metrics: dict[str, MetricFn],
) -> dict[str, float]:
    """여러 메트릭으로 모델을 평가합니다."""
    results: dict[str, float] = {}
    for name, metric_fn in metrics.items():
        results[name] = metric_fn(list(y_true), list(y_pred))
    return results

# 제네릭 결과 컨테이너
T = TypeVar("T")

class Result(Generic[T]):
    """성공/실패를 표현하는 결과 타입"""

    def __init__(self, value: T | None = None, error: str | None = None) -> None:
        self._value = value
        self._error = error

    @property
    def is_ok(self) -> bool:
        return self._error is None

    def unwrap(self) -> T:
        if self._error is not None:
            raise ValueError(self._error)
        assert self._value is not None
        return self._value

# 사용
def load_model(path: str) -> Result[dict[str, float]]:
    """모델 가중치를 로딩합니다."""
    try:
        weights = {"layer1": 0.5, "layer2": 0.3}
        return Result(value=weights)
    except FileNotFoundError:
        return Result(error=f"파일을 찾을 수 없습니다: {path}")

result = load_model("model.pt")
if result.is_ok:
    weights = result.unwrap()
    print(f"로딩 성공: {len(weights)}개 레이어")

mypy로 정적 타입 검사

# mypy 설치
pip install mypy

# 타입 검사 실행
mypy my_script.py

# 엄격 모드 (모든 함수에 타입 필수)
mypy --strict my_script.py
# pyproject.toml에 mypy 설정
# [tool.mypy]
# python_version = "3.12"
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true
타입 힌트는 런타임에 무시됩니다. Python 인터프리터는 타입 어노테이션을 실행하지 않으므로 성능 영향이 없습니다. from __future__ import annotations를 사용하면 어노테이션 평가조차 지연됩니다.
실무에서는 공개 API(함수 시그니처)에 우선 적용하고, 내부 구현은 점진적으로 추가합니다. mypy의 --strict 모드를 바로 적용하기보다, 새 코드부터 적용하는 것이 현실적입니다.

체크리스트

  • 함수에 매개변수 타입과 반환 타입을 표기할 수 있다
  • Optional, Union, Literal을 적절히 사용할 수 있다
  • GenericTypeVar로 재사용 가능한 타입을 정의할 수 있다
  • TYPE_CHECKING으로 순환 참조를 해결할 수 있다
  • mypy로 정적 타입 검사를 실행할 수 있다

다음 문서