본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 11장: typing 고급 기능 실전 활용
2026년 2월 5일·프로그래밍·

11장: typing 고급 기능 실전 활용

Python 3.12~3.13의 typing 모듈 고급 기능을 다룹니다. TypedDict, Protocol, override, dataclass_transform, TypeGuard, TypeIs 등 실전 타입 시스템을 안내합니다.

13분926자12개 섹션
pythontypescriptperformancedevtoolsconcurrency
공유
python-trends11 / 13
12345678910111213
이전10장: Ruff와 ty - 차세대 린터, 포매터, 타입 체커다음12장: AI 개발에서의 Python 활용

타입 시스템의 성숙

Python의 타입 시스템은 3.5에서 typing 모듈이 도입된 이후 매 버전마다 발전해왔습니다. 3.12~3.13에서는 2장에서 다룬 PEP 695(타입 파라미터 문법)와 함께 여러 고급 기능이 추가되거나 안정화되었습니다.

이 장에서는 실무에서 자주 사용되는 고급 타입 기능들을 다룹니다.

Protocol: 구조적 서브타이핑

Protocol은 "덕 타이핑(duck typing)"을 타입 시스템에서 공식화한 것입니다. 클래스가 특정 인터페이스를 구현하는지를 상속 관계가 아닌 구조(메서드와 속성의 존재)로 판단합니다.

Protocol 정의와 사용
python
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 Protocol

@runtime_checkable 데코레이터를 사용하면 isinstance() 검사가 가능해집니다.

runtime_checkable
python
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
 
# 주의: 런타임 검사는 메서드 존재 여부만 확인
# 시그니처는 검사하지 않음

제네릭 Protocol

2장에서 다룬 PEP 695 문법과 결합하면 더 깔끔합니다.

제네릭 Protocol (PEP 695)
python
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: 구조화된 딕셔너리

TypedDict는 딕셔너리의 키와 값의 타입을 정의합니다. JSON API 응답이나 설정 파일을 다룰 때 유용합니다.

TypedDict 기본 사용
python
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"])

TypedDict 상속

TypedDict 상속
python
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: DatabaseConfig

ReadOnly (Python 3.13)

Python 3.13에서는 TypedDict 필드에 ReadOnly 표기를 할 수 있습니다.

ReadOnly TypedDict (3.13)
python
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 필드

override 데코레이터 (PEP 698)

Python 3.12에서 도입된 @override 데코레이터는 메서드가 부모 클래스의 메서드를 오버라이드한다는 의도를 명시합니다.

override 데코레이터
python
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가 붙은 자식 클래스의 메서드에서 타입 에러가 발생하여 누락된 수정을 바로 발견할 수 있습니다.

TypeGuard와 TypeIs

타입 좁히기(type narrowing)를 위한 두 가지 도구입니다.

TypeGuard

TypeGuard
python
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")

TypeIs (Python 3.13)

Python 3.13에서 도입된 TypeIs는 TypeGuard보다 더 정밀한 타입 좁히기를 제공합니다.

TypeIs (3.13)
python
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)
Info

TypeGuard와 TypeIs의 핵심 차이는 else 분기에서의 동작입니다. TypeGuard는 False일 때 원래 타입을 유지하지만, TypeIs는 해당 타입을 제외한 나머지로 좁힙니다. 대부분의 경우 TypeIs가 더 직관적이므로, Python 3.13 이상을 대상으로 한다면 TypeIs를 사용하는 것이 좋습니다.

dataclass_transform (PEP 681)

dataclass_transform은 dataclass와 유사한 동작을 하는 커스텀 데코레이터나 베이스 클래스에 대해 타입 체커가 올바르게 추론하도록 돕습니다.

dataclass_transform
python
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와 메타데이터

Annotated를 사용하면 타입에 추가 메타데이터를 부착할 수 있습니다. Pydantic, FastAPI, typer 같은 프레임워크에서 활용됩니다.

Annotated 활용
python
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와 NoReturn

Never는 Python 3.11에서 도입된 타입으로, 값이 존재할 수 없음을 나타냅니다. NoReturn의 더 일반적인 형태입니다.

Never 타입
python
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(완전성 검사)에 사용됩니다. 패턴 매칭에서 모든 경우를 처리했는지 타입 체커가 확인할 수 있습니다.

Unpack과 TypedDict를 활용한 kwargs 타입

Python 3.12에서 안정화된 Unpack은 TypedDict와 결합하여 키워드 인자의 타입을 정밀하게 정의할 수 있습니다.

Unpack으로 kwargs 타입 정의
python
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)

실전 패턴: 타입 안전한 이벤트 시스템

지금까지 다룬 기능을 결합한 실전 예시입니다.

타입 안전한 이벤트 시스템
python
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 모듈은 단순한 타입 힌트를 넘어 강력한 타입 시스템으로 발전했습니다.

  • Protocol은 구조적 서브타이핑으로 유연한 인터페이스를 정의합니다
  • TypedDict는 딕셔너리의 구조를 타입으로 표현합니다
  • override 데코레이터는 리팩터링 안전성을 높입니다
  • TypeIs(3.13)는 TypeGuard보다 정밀한 타입 좁히기를 제공합니다
  • dataclass_transform은 커스텀 데이터 클래스 도구와 타입 체커를 연결합니다
  • 이 기능들을 조합하면 타입 안전한 도메인 모델을 구축할 수 있습니다

다음 장 미리보기

12장에서는 AI/ML 개발에서의 Python 활용을 다룹니다. PyTorch, Hugging Face 생태계와 Python의 관계, AI 개발에 특화된 Python 패턴, 그리고 free-threaded Python이 AI 워크로드에 미치는 영향을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#python#typescript#performance#devtools#concurrency

관련 글

프로그래밍

12장: AI 개발에서의 Python 활용

AI/ML 개발에서 Python이 차지하는 위치와 최신 트렌드를 다룹니다. PyTorch 생태계, LLM 개발 도구, 타입 안전한 AI 파이프라인, free-threaded Python의 AI 활용을 살펴봅니다.

2026년 2월 7일·13분
프로그래밍

10장: Ruff와 ty - 차세대 린터, 포매터, 타입 체커

Astral의 Ruff(린터/포매터)와 ty(타입 체커)를 다룹니다. 기존 도구 대체, 설정 방법, 규칙 커스터마이징, IDE 통합, 프로젝트 도입 전략을 안내합니다.

2026년 2월 3일·14분
프로그래밍

13장: Python 3.13 마이그레이션 실전 가이드

기존 프로젝트를 Python 3.13으로 업그레이드하는 실전 가이드입니다. 호환성 체크리스트, 단계별 전략, 주요 라이브러리 호환성, 도구 전환 계획을 다룹니다.

2026년 2월 9일·17분
이전 글10장: Ruff와 ty - 차세대 린터, 포매터, 타입 체커
다음 글12장: AI 개발에서의 Python 활용

댓글

목차

약 13분 남음
  • 타입 시스템의 성숙
  • Protocol: 구조적 서브타이핑
    • runtime_checkable Protocol
    • 제네릭 Protocol
  • TypedDict: 구조화된 딕셔너리
    • TypedDict 상속
    • ReadOnly (Python 3.13)
  • override 데코레이터 (PEP 698)
  • TypeGuard와 TypeIs
    • TypeGuard
    • TypeIs (Python 3.13)
  • dataclass_transform (PEP 681)
  • Annotated와 메타데이터
  • Never와 NoReturn
  • Unpack과 TypedDict를 활용한 kwargs 타입
  • 실전 패턴: 타입 안전한 이벤트 시스템
  • 정리
  • 다음 장 미리보기