본문으로 건너뛰기
김신건의 로그

[Python] typing: 타입 힌트 기초

· 수정 · 📖 약 2분 · 656자/단어 #python #typing #type-hints #mypy #advanced
python typing, type hints, Optional, Union, Generic, TypeVar

정의

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는 모든 타입과 호환 → 남용하면 타입 시스템 무력화
  • listtuple의 차이 (가변/불변, 길이 가변/고정) 어노테이션에서 정확히
  • 함수 인수 변경에 따른 호출자 영향이 컴파일 타임 검출 안 됨 → 타입 체커 필수

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기