타입 힌트
학습 목표
- 기본 타입 어노테이션을 함수와 변수에 적용할 수 있다
Optional,Union,Literal등 고급 타입을 활용할 수 있다Generic과TypeVar로 재사용 가능한 타입을 정의할 수 있다TYPE_CHECKING으로 순환 참조를 해결할 수 있다
왜 중요한가
타입 힌트(Type Hint)는 Python 3.5부터 도입된 정적 타입 표기 시스템입니다. ML/DL 프로젝트가 커질수록 함수의 입출력 타입이 불분명해지기 쉽습니다. 타입 힌트는 코드 문서화, IDE 자동완성, 정적 분석(mypy)을 통해 버그를 사전에 방지합니다. FastAPI, Pydantic 등 현대 Python 프레임워크는 타입 힌트를 핵심 기능으로 활용합니다.기본 타입 어노테이션
변수와 함수의 타입 표기
Copy
# 변수 타입 어노테이션
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
컬렉션 타입
Copy
# 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는 더 이상 필요하지 않습니다.함수 매개변수 패턴
Copy
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
Copy
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
Copy
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
Copy
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과 순환 참조 해결
Copy
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를 파일 최상단에 추가하면 모든 타입 어노테이션이 문자열로 처리되어, 아직 정의되지 않은 클래스도 참조할 수 있습니다.타입 가드와 타입 좁히기
Copy
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에서의 활용
Copy
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로 정적 타입 검사
Copy
# mypy 설치
pip install mypy
# 타입 검사 실행
mypy my_script.py
# 엄격 모드 (모든 함수에 타입 필수)
mypy --strict my_script.py
Copy
# 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을 적절히 사용할 수 있다 -
Generic과TypeVar로 재사용 가능한 타입을 정의할 수 있다 -
TYPE_CHECKING으로 순환 참조를 해결할 수 있다 - mypy로 정적 타입 검사를 실행할 수 있다

