[Python] typing: 타입 힌트 기초
정의
Python 타입 힌트(PEP 484)는 런타임에 강제되지 않는 정적 어노테이션이다. mypy, pyright, pyre 같은 타입 체커가 정적 분석에 사용. 런타임은 어노테이션을 무시 (단, 일부 프레임워크가 introspection으로 활용: pydantic, FastAPI, dataclass).
기본
def greet(name: str, count: int = 1) -> str:
return f"Hello {name}" * count
age: int = 30
names: list[str] = ["Alice", "Bob"]
함수 시그니처와 변수 어노테이션. :는 타입, =은 기본값.
흔한 타입
from typing import Optional, Union, Callable, Any, ClassVar
x: int # 정수
s: str # 문자열
b: bool # 불리언
nums: list[int] # 정수 리스트 (3.9+ built-in 제네릭)
mapping: dict[str, int] # 키 str, 값 int
items: tuple[int, str, bool] # 고정 길이
coords: tuple[int, ...] # 가변 길이 (모두 int)
flag: bool | None # 3.10+ union (Optional 등가)
flag: Optional[bool] # 3.9 이하
result: Union[int, str] # 3.9 이하 union
fn: Callable[[int, int], int] # 인수 (int, int), 반환 int
data: Any # 어떤 타입이든
Optional vs Union[X, None]
같은 의미. Optional[X]는 X | None의 별칭.
def find(uid: int) -> User | None:
...
# 호출 측은 None 체크 강제
u = find(1)
if u is not None:
print(u.name)
list, dict 같은 built-in 제네릭 (3.9+)
PEP 585로 built-in 컨테이너 자체가 제네릭이 됨. typing.List/Dict/Set/Tuple은 더 이상 필요 X.
# 3.9 이상 (권장)
def f(xs: list[int]) -> dict[str, int]: ...
# 3.8 이하
from typing import List, Dict
def f(xs: List[int]) -> Dict[str, int]: ...
새 코드는 built-in 형식.
Union 문법 (3.10+)
# 3.10+
def f(x: int | str) -> bool | None: ...
# 3.9 이하
from typing import Union, Optional
def f(x: Union[int, str]) -> Optional[bool]: ...
TypeVar: 제네릭
from typing import TypeVar
T = TypeVar("T")
def first(xs: list[T]) -> T:
return xs[0]
first([1, 2, 3]) # int
first(["a", "b"]) # str
T는 호출 시 구체 타입으로 추론. 입력과 반환이 같은 타입임을 표현.
bound와 constraints
from typing import TypeVar
# bound: T는 Number 또는 그 서브타입
N = TypeVar("N", bound=float)
def double(x: N) -> N:
return x * 2
# constraints: T는 이 셋 중 하나
S = TypeVar("S", int, str, bytes)
def echo(x: S) -> S: return x
제네릭 클래스
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, x: T) -> None:
self._items.append(x)
def pop(self) -> T:
return self._items.pop()
s: Stack[int] = Stack()
s.push(1)
n: int = s.pop()
3.12+ PEP 695 새 문법:
class Stack[T]: # Generic 명시 불필요
def push(self, x: T) -> None: ...
def pop(self) -> T: ...
def first[T](xs: list[T]) -> T: ...
Callable
from typing import Callable
# 인수 타입 명시
handler: Callable[[int, str], bool] = my_handler
# 인수 타입 무관
any_fn: Callable[..., int] = my_fn
# 인수 없음
no_arg: Callable[[], None] = my_fn
ParamSpec(3.10+)은 데코레이터에서 시그니처 전달 시 필수.
from typing import Callable, ParamSpec, TypeVar
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def trace(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
@trace
def add(a: int, b: int) -> int:
return a + b
Literal: 특정 값만
from typing import Literal
def color(mode: Literal["light", "dark", "auto"]) -> str: ...
color("light") # OK
color("blue") # 타입 체커 에러
API 옵션 enum 대신 가벼운 대안.
Final, ClassVar
from typing import Final, ClassVar
class Config:
VERSION: Final = "1.0" # 재할당 금지 (mypy 강제)
instances: ClassVar[int] = 0 # 클래스 변수 (인스턴스 어노테이션 아님)
ClassVar는 @dataclass가 클래스 변수와 필드를 구분할 때도 사용.
TypeAlias
from typing import TypeAlias
UserId: TypeAlias = int
Email: TypeAlias = str
def find(uid: UserId) -> User | None: ...
3.12+ 키워드 문법:
type UserId = int
type Email = str
가독성과 도메인 의미 부여. 런타임엔 그냥 int.
NewType
별개 타입처럼 구분(같은 표현이지만 타입 체커는 다른 타입으로 취급).
from typing import NewType
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def get_user(uid: UserId) -> User: ...
get_user(123) # 타입 체커 에러 (NewType 변환 필요)
get_user(UserId(123)) # OK
ID 혼동 방지에 강력.
런타임 도구
def f(x: int, y: str = "") -> bool: ...
print(f.__annotations__)
# {'x': <class 'int'>, 'y': <class 'str'>, 'return': <class 'bool'>}
import typing
print(typing.get_type_hints(f)) # 더 안전 (전방 참조 해결)
get_type_hints는 "User" 같은 문자열(전방 참조)을 실제 타입으로 변환.
전방 참조 (Forward Reference)
class Node:
def add_child(self, child: "Node") -> None:
...
자기 자신 또는 아래에 정의된 클래스를 참조할 때 따옴표로 감싸 문자열로.
from __future__ import annotations(3.7+)를 파일 맨 위에 두면 모든 어노테이션이 자동으로 문자열 처리 → 따옴표 불필요. 단, 런타임 introspection 시 get_type_hints 필요.
NoReturn / Never (3.11+)
함수가 절대 반환하지 않음을 표현 (예외, sys.exit, 무한 루프).
from typing import NoReturn
def fail(msg: str) -> NoReturn:
raise RuntimeError(msg)
# 3.11+
from typing import Never
def crash() -> Never: ...
타입 체커가 흐름 분석에 활용 (if x is None: fail(); use(x)에서 use 다음 코드가 unreachable로 판단).
Self (3.11+)
from typing import Self
class Builder:
def step(self) -> Self: # 서브클래스에서도 자기 자신 반환
return self
class CustomBuilder(Builder):
pass
CustomBuilder().step() # CustomBuilder로 추론
이전엔 TypeVar("Self", bound="Builder")로 표현했던 패턴.
타입 체커 사용
pip install mypy
mypy myapp/
pip install pyright
pyright myapp/
mypy.ini / pyproject.toml에 설정. CI에서 차단 단계로 두면 타입 안전성 강제.
함정
- 어노테이션이 런타임 강제 아님 (
age: int = "30"은 실행됨) Any는 모든 타입과 호환 → 남용하면 타입 시스템 무력화list와tuple의 차이 (가변/불변, 길이 가변/고정) 어노테이션에서 정확히- 함수 인수 변경에 따른 호출자 영향이 컴파일 타임 검출 안 됨 → 타입 체커 필수
💬 댓글