Python 3.12~3.13의 typing 모듈 고급 기능을 다룹니다. TypedDict, Protocol, override, dataclass_transform, TypeGuard, TypeIs 등 실전 타입 시스템을 안내합니다.
Python의 타입 시스템은 3.5에서 typing 모듈이 도입된 이후 매 버전마다 발전해왔습니다. 3.12~3.13에서는 2장에서 다룬 PEP 695(타입 파라미터 문법)와 함께 여러 고급 기능이 추가되거나 안정화되었습니다.
이 장에서는 실무에서 자주 사용되는 고급 타입 기능들을 다룹니다.
Protocol은 "덕 타이핑(duck typing)"을 타입 시스템에서 공식화한 것입니다. 클래스가 특정 인터페이스를 구현하는지를 상속 관계가 아닌 구조(메서드와 속성의 존재)로 판단합니다.
from typing import Protocol, runtime_checkable
class Renderable(Protocol):
def render(self) -> str: ...
class HTMLElement:
def __init__(self, tag: str, content: str) -> None:
self.tag = tag
self.content = content
def render(self) -> str:
return "<" + self.tag + ">" + self.content + "</" + self.tag + ">"
class MarkdownHeading:
def __init__(self, level: int, text: str) -> None:
self.level = level
self.text = text
def render(self) -> str:
return "#" * self.level + " " + self.text
# HTMLElement과 MarkdownHeading은 Renderable을 상속하지 않지만,
# render() 메서드를 가지고 있으므로 Renderable로 사용 가능
def render_all(items: list[Renderable]) -> str:
return "\n".join(item.render() for item in items)
elements: list[Renderable] = [
HTMLElement("p", "Hello"),
MarkdownHeading(2, "World"),
]@runtime_checkable 데코레이터를 사용하면 isinstance() 검사가 가능해집니다.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
import io
f = io.StringIO()
print(isinstance(f, Closeable)) # True
# 주의: 런타임 검사는 메서드 존재 여부만 확인
# 시그니처는 검사하지 않음2장에서 다룬 PEP 695 문법과 결합하면 더 깔끔합니다.
from typing import Protocol
class Repository[T, ID](Protocol):
def find(self, id: ID) -> T | None: ...
def save(self, entity: T) -> T: ...
def delete(self, id: ID) -> bool: ...
class Serializable[T](Protocol):
def serialize(self) -> bytes: ...
@classmethod
def deserialize(cls, data: bytes) -> T: ...TypedDict는 딕셔너리의 키와 값의 타입을 정의합니다. JSON API 응답이나 설정 파일을 다룰 때 유용합니다.
from typing import TypedDict, NotRequired, Required
class UserProfile(TypedDict):
name: str
email: str
age: int
bio: NotRequired[str] # 선택적 필드
class APIResponse(TypedDict):
status: str
data: list[UserProfile]
total: int
next_cursor: NotRequired[str | None]
def process_response(response: APIResponse) -> None:
for user in response["data"]:
print(user["name"] + ": " + user["email"])
if "bio" in user:
print(" Bio: " + user["bio"])class BaseConfig(TypedDict):
debug: bool
log_level: str
class DatabaseConfig(BaseConfig):
host: str
port: int
database: str
class AppConfig(TypedDict):
app_name: str
version: str
db: DatabaseConfigPython 3.13에서는 TypedDict 필드에 ReadOnly 표기를 할 수 있습니다.
from typing import TypedDict, ReadOnly
class ImmutableConfig(TypedDict):
name: ReadOnly[str]
version: ReadOnly[str]
debug: bool # 이것만 변경 가능
config: ImmutableConfig = {
"name": "MyApp",
"version": "1.0.0",
"debug": True,
}
config["debug"] = False # OK
# config["name"] = "Other" # 타입 체커 에러: ReadOnly 필드Python 3.12에서 도입된 @override 데코레이터는 메서드가 부모 클래스의 메서드를 오버라이드한다는 의도를 명시합니다.
from typing import override
class Animal:
def speak(self) -> str:
return "..."
def move(self) -> str:
return "moving"
class Dog(Animal):
@override
def speak(self) -> str:
return "Woof"
@override
def move(self) -> str:
return "running"
# 오타: speak를 spek으로 잘못 씀
# @override
# def spek(self) -> str: # 타입 체커 에러!
# return "Woof" # 부모에 spek 메서드가 없음@override의 가치는 리팩터링 안전성입니다. 부모 클래스에서 메서드 이름을 변경하면, @override가 붙은 자식 클래스의 메서드에서 타입 에러가 발생하여 누락된 수정을 바로 발견할 수 있습니다.
타입 좁히기(type narrowing)를 위한 두 가지 도구입니다.
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in val)
def process(data: list[object]) -> None:
if is_string_list(data):
# 여기서 data는 list[str]로 좁혀짐
print(", ".join(data))
else:
print("Not all strings")Python 3.13에서 도입된 TypeIs는 TypeGuard보다 더 정밀한 타입 좁히기를 제공합니다.
from typing import TypeIs
def is_str(val: object) -> TypeIs[str]:
return isinstance(val, str)
def process(val: int | str) -> None:
if is_str(val):
# val은 str로 좁혀짐
print(val.upper())
else:
# TypeIs 덕분에 여기서 val은 int로 좁혀짐
# TypeGuard로는 이 부분이 int | str로 남음
print(val + 1)TypeGuard와 TypeIs의 핵심 차이는 else 분기에서의 동작입니다. TypeGuard는 False일 때 원래 타입을 유지하지만, TypeIs는 해당 타입을 제외한 나머지로 좁힙니다. 대부분의 경우 TypeIs가 더 직관적이므로, Python 3.13 이상을 대상으로 한다면 TypeIs를 사용하는 것이 좋습니다.
dataclass_transform은 dataclass와 유사한 동작을 하는 커스텀 데코레이터나 베이스 클래스에 대해 타입 체커가 올바르게 추론하도록 돕습니다.
from typing import dataclass_transform
from dataclasses import dataclass, field
@dataclass_transform()
def my_dataclass(cls):
"""커스텀 데이터클래스 데코레이터"""
return dataclass(cls)
@my_dataclass
class User:
name: str
email: str
age: int = 0
# 타입 체커가 __init__(name: str, email: str, age: int = 0)을 인식
user = User(name="Alice", email="alice@example.com")이 기능은 SQLAlchemy, Pydantic, attrs 같은 라이브러리에서 특히 유용합니다. 이 라이브러리들은 자체적인 모델 정의 방식을 사용하지만, dataclass_transform을 통해 타입 체커와 IDE가 올바른 타입 추론을 제공합니다.
Annotated를 사용하면 타입에 추가 메타데이터를 부착할 수 있습니다. Pydantic, FastAPI, typer 같은 프레임워크에서 활용됩니다.
from typing import Annotated
from dataclasses import dataclass
# 메타데이터 정의
class MaxLen:
def __init__(self, max_length: int) -> None:
self.max_length = max_length
class MinVal:
def __init__(self, minimum: int) -> None:
self.minimum = minimum
# 타입에 메타데이터 부착
type Username = Annotated[str, MaxLen(50)]
type Age = Annotated[int, MinVal(0)]
type Email = Annotated[str, MaxLen(255)]
@dataclass
class UserInput:
username: Username
age: Age
email: Email
# 런타임에 메타데이터 접근
import typing
hints = typing.get_type_hints(UserInput, include_extras=True)
username_type = hints["username"]
# Annotated[str, MaxLen(50)]
metadata = typing.get_args(username_type)
# (str, MaxLen(50))Never는 Python 3.11에서 도입된 타입으로, 값이 존재할 수 없음을 나타냅니다. NoReturn의 더 일반적인 형태입니다.
from typing import Never, assert_never
type Shape = Circle | Rectangle | Triangle
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
case _ as unreachable:
assert_never(unreachable)
# Shape에 새 타입이 추가되면
# 이 줄에서 타입 에러 발생assert_never는 exhaustiveness check(완전성 검사)에 사용됩니다. 패턴 매칭에서 모든 경우를 처리했는지 타입 체커가 확인할 수 있습니다.
Python 3.12에서 안정화된 Unpack은 TypedDict와 결합하여 키워드 인자의 타입을 정밀하게 정의할 수 있습니다.
from typing import TypedDict, Unpack
class RequestOptions(TypedDict, total=False):
timeout: float
headers: dict[str, str]
verify_ssl: bool
max_retries: int
def make_request(
url: str,
method: str = "GET",
**kwargs: Unpack[RequestOptions],
) -> dict:
timeout = kwargs.get("timeout", 30.0)
headers = kwargs.get("headers", {})
# ...
return {}
# 타입 체커가 키워드 인자의 이름과 타입을 검증
make_request(
"https://api.example.com",
timeout=5.0,
headers={"Authorization": "Bearer token"},
)
# 타입 에러: unknown_option은 RequestOptions에 없음
# make_request("https://api.example.com", unknown_option=True)지금까지 다룬 기능을 결합한 실전 예시입니다.
from typing import Protocol, TypedDict, override
from dataclasses import dataclass
from collections.abc import Callable
# 이벤트 정의
@dataclass
class Event:
source: str
@dataclass
class UserEvent(Event):
user_id: int
@dataclass
class UserCreated(UserEvent):
username: str
@dataclass
class UserDeleted(UserEvent):
reason: str
# 이벤트 핸들러 Protocol
class EventHandler[E: Event](Protocol):
def handle(self, event: E) -> None: ...
# 구체적 핸들러
class UserCreatedHandler:
def handle(self, event: UserCreated) -> None:
print("User created: " + event.username)
class UserDeletedHandler:
def handle(self, event: UserDeleted) -> None:
print("User deleted: " + str(event.user_id) + " reason: " + event.reason)
# 이벤트 버스
class EventBus:
def __init__(self) -> None:
self._handlers: dict[type[Event], list[Callable]] = {}
def subscribe[E: Event](
self,
event_type: type[E],
handler: EventHandler[E],
) -> None:
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler.handle)
def publish(self, event: Event) -> None:
event_type = type(event)
for handler in self._handlers.get(event_type, []):
handler(event)
# 사용
bus = EventBus()
bus.subscribe(UserCreated, UserCreatedHandler())
bus.subscribe(UserDeleted, UserDeletedHandler())
bus.publish(UserCreated(source="api", user_id=1, username="alice"))
bus.publish(UserDeleted(source="admin", user_id=1, reason="request"))Python의 typing 모듈은 단순한 타입 힌트를 넘어 강력한 타입 시스템으로 발전했습니다.
12장에서는 AI/ML 개발에서의 Python 활용을 다룹니다. PyTorch, Hugging Face 생태계와 Python의 관계, AI 개발에 특화된 Python 패턴, 그리고 free-threaded Python이 AI 워크로드에 미치는 영향을 살펴봅니다.
이 글이 도움이 되셨나요?
AI/ML 개발에서 Python이 차지하는 위치와 최신 트렌드를 다룹니다. PyTorch 생태계, LLM 개발 도구, 타입 안전한 AI 파이프라인, free-threaded Python의 AI 활용을 살펴봅니다.
Astral의 Ruff(린터/포매터)와 ty(타입 체커)를 다룹니다. 기존 도구 대체, 설정 방법, 규칙 커스터마이징, IDE 통합, 프로젝트 도입 전략을 안내합니다.
기존 프로젝트를 Python 3.13으로 업그레이드하는 실전 가이드입니다. 호환성 체크리스트, 단계별 전략, 주요 라이브러리 호환성, 도구 전환 계획을 다룹니다.