Python 3.12에서 도입된 PEP 695 타입 파라미터 문법을 상세히 분석합니다. TypeVar의 간결한 선언, 제네릭 클래스와 함수의 새 문법, type 별칭을 다룹니다.
Python에 타입 힌트가 도입된 이후, 제네릭(Generic) 타입은 코드의 재사용성과 타입 안전성을 높이는 핵심 도구였습니다. 그러나 기존 문법은 "덧붙여진(bolted-on)" 느낌이 강했습니다.
기존 방식에서는 제네릭 클래스를 만들기 위해 세 가지를 동시에 처리해야 했습니다.
from typing import TypeVar, Generic, Sequence
T = TypeVar("T") # 1. TypeVar 선언 (이름을 두 번 써야 함)
class Stack(Generic[T]): # 2. Generic 상속
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def first(seq: Sequence[T]) -> T: # 3. 함수에서도 동일한 T 사용
return seq[0]이 코드에는 몇 가지 불편한 점이 있습니다.
첫째, T = TypeVar("T")에서 변수 이름과 문자열 인자를 반복해야 합니다. 이름이 일치하지 않으면 런타임에서 혼란이 생깁니다. 둘째, Generic[T]를 명시적으로 상속해야 합니다. 셋째, TypeVar가 모듈 수준에서 선언되므로 해당 변수의 유효 범위가 불명확합니다. 어떤 클래스나 함수가 이 TypeVar를 사용하는지 코드를 읽어봐야 알 수 있습니다.
Python 3.12에서 도입된 PEP 695는 이 문제를 근본적으로 해결합니다. 대괄호를 사용한 인라인 선언 방식으로, 타입 파라미터를 사용할 위치에서 바로 선언합니다.
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()TypeVar 임포트도, Generic 상속도 사라졌습니다. T는 클래스 선언부의 대괄호 안에서 바로 정의되며, 그 유효 범위는 해당 클래스로 한정됩니다.
from collections.abc import Sequence
def first[T](seq: Sequence[T]) -> T:
return seq[0]
def pair[T, U](a: T, b: U) -> tuple[T, U]:
return (a, b)함수 이름 뒤에 대괄호로 타입 파라미터를 선언합니다. 여러 타입 파라미터가 필요하면 쉼표로 구분합니다.
타입 파라미터에 상한(bound)을 설정하거나 특정 타입으로 제한할 수 있습니다.
from collections.abc import Hashable, Comparable
# 상한(bound): T는 Hashable의 하위 타입이어야 함
def hash_value[T: Hashable](item: T) -> int:
return hash(item)
# 제약조건(constraint): T는 int 또는 float만 가능
def add[T: (int, float)](a: T, b: T) -> T:
return a + b기존 문법에서는 TypeVar("T", bound=Hashable) 또는 TypeVar("T", int, float)로 작성해야 했습니다. 새 문법은 콜론 뒤에 바운드를, 튜플로 제약조건을 직접 명시합니다.
PEP 695는 가변 길이 타입 파라미터(TypeVarTuple)와 파라미터 사양(ParamSpec)에 대해서도 새로운 문법을 제공합니다.
# 기존 문법
from typing import TypeVarTuple, Unpack
Ts = TypeVarTuple("Ts")
def concat(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
return args
# 새 문법 (Python 3.12+)
def concat[*Ts](*args: *Ts) -> tuple[*Ts]:
return args별표(*)를 접두사로 사용하여 TypeVarTuple을 선언합니다. 이는 가변 개수의 타입을 받을 수 있는 제네릭을 정의할 때 유용합니다.
from collections.abc import Callable
import functools
# 기존 문법
from typing import ParamSpec
P = ParamSpec("P")
# 새 문법 (Python 3.12+)
def logged[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("Calling " + func.__name__)
return func(*args, **kwargs)
return wrapper이중 별표(**)를 접두사로 사용하여 ParamSpec을 선언합니다. 데코레이터 패턴에서 원본 함수의 시그니처를 보존할 때 필수적인 기능입니다.
Python 3.12는 타입 별칭을 선언하는 type 문도 함께 도입했습니다.
# 기존 문법
from typing import TypeAlias, Union
Vector: TypeAlias = list[float]
NumberOrStr: TypeAlias = Union[int, float, str]
# 새 문법 (Python 3.12+)
type Vector = list[float]
type NumberOrStr = int | float | str
# 제네릭 타입 별칭
type Matrix[T] = list[list[T]]
type Handler[**P, R] = Callable[P, R]type 문의 장점은 명확합니다.
첫째, TypeAlias 임포트가 불필요합니다. 둘째, 제네릭 타입 별칭을 대괄호로 직접 선언할 수 있습니다. 셋째, type 문의 우변은 지연 평가(lazy evaluation)됩니다. 이는 순환 참조 문제를 해결하는 데 도움이 됩니다.
# 기존 문법에서는 문자열 리터럴로 순환 참조를 해결해야 했음
class TreeNode:
children: list["TreeNode"] # 전방 참조(forward reference)
# type 문은 지연 평가되므로 자연스럽게 해결
type TreeChildren = list[TreeNode]
class TreeNode:
children: TreeChildren기존 코드를 새 문법으로 리팩터링하는 실전 예시를 살펴봅니다.
from typing import TypeVar, Generic, Protocol
from abc import abstractmethod
T = TypeVar("T")
ID = TypeVar("ID")
class Repository(Generic[T, ID]):
@abstractmethod
def find_by_id(self, id: ID) -> T | None: ...
@abstractmethod
def save(self, entity: T) -> T: ...
@abstractmethod
def delete(self, id: ID) -> bool: ...from abc import abstractmethod
class Repository[T, ID]:
@abstractmethod
def find_by_id(self, id: ID) -> T | None: ...
@abstractmethod
def save(self, entity: T) -> T: ...
@abstractmethod
def delete(self, id: ID) -> bool: ...
# 구체 구현
class UserRepository(Repository[User, int]):
def find_by_id(self, id: int) -> User | None:
return self._db.query(User).filter_by(id=id).first()
def save(self, entity: User) -> User:
self._db.add(entity)
self._db.commit()
return entity
def delete(self, id: int) -> bool:
user = self.find_by_id(id)
if user:
self._db.delete(user)
self._db.commit()
return True
return Falsefrom collections.abc import Callable
from dataclasses import dataclass
@dataclass
class Event:
timestamp: float
@dataclass
class UserCreated(Event):
user_id: int
username: str
@dataclass
class OrderPlaced(Event):
order_id: int
amount: float
class EventBus[E: Event]:
def __init__(self) -> None:
self._handlers: list[Callable[[E], None]] = []
def subscribe(self, handler: Callable[[E], None]) -> None:
self._handlers.append(handler)
def publish(self, event: E) -> None:
for handler in self._handlers:
handler(event)
# 타입 안전한 이벤트 버스
user_events = EventBus[UserCreated]()
order_events = EventBus[OrderPlaced]()PEP 695 문법의 도구 지원은 빠르게 확산되고 있습니다.
| 도구 | 지원 상태 | 비고 |
|---|---|---|
| mypy | 1.7+ 지원 | 대부분의 기능 지원 |
| Pyright | 완전 지원 | 가장 빠른 지원 |
| ty | 완전 지원 | Astral의 새 타입 체커 |
| Ruff | 완전 지원 | 린트 규칙 적용 |
| VS Code | Pylance를 통해 지원 | 자동완성, 타입 추론 |
| PyCharm | 2024.1+ 지원 | 리팩터링 도구 포함 |
mypy를 사용하는 경우, PEP 695 문법을 완전히 지원하려면 mypy 1.7 이상이 필요합니다. pyproject.toml에서 python_version을 3.12 이상으로 설정해야 새 문법이 정상적으로 검사됩니다.
기존 프로젝트를 새 문법으로 전환할 때 고려할 사항입니다.
새 문법과 기존 문법은 공존할 수 있습니다. 한 프로젝트 내에서 두 스타일을 혼용해도 문제가 없으므로, 새로 작성하는 코드부터 새 문법을 적용하는 것이 안전합니다.
현재 기존 문법을 새 문법으로 자동 변환하는 공식 도구는 없습니다. 그러나 Ruff의 UP040 규칙은 TypeAlias를 type 문으로 변환하는 것을 제안합니다.
새 문법은 Python 3.12 이상에서만 동작합니다. 3.11 이하를 지원해야 하는 라이브러리는 typing_extensions 패키지와 조건부 임포트를 고려해야 합니다.
import sys
if sys.version_info >= (3, 12):
type Vector = list[float]
else:
from typing import TypeAlias
Vector: TypeAlias = list[float]PEP 695는 Python 타입 시스템의 사용성을 크게 개선한 변화입니다. 핵심 포인트를 요약하면 다음과 같습니다.
Generic[T] 상속이 불필요해졌습니다type 문으로 타입 별칭을 선언하며, 지연 평가로 순환 참조 문제를 해결합니다3장에서는 Python 3.10에서 도입된 구조적 패턴 매칭(match/case)의 실전 활용법을 다룹니다. 단순한 값 비교를 넘어, 데이터 구조 분해, 클래스 매칭, 가드 조건까지 실무에서 패턴 매칭을 효과적으로 활용하는 방법을 살펴봅니다.
이 글이 도움이 되셨나요?
Python의 match/case 문을 실전에서 활용하는 방법을 다룹니다. 시퀀스, 매핑, 클래스 패턴부터 가드 조건, 중첩 패턴까지 실무 코드로 익힙니다.
Python 3.12와 3.13에서 도입된 핵심 변화를 조망하고, 타입 시스템 개선부터 GIL 제거, 차세대 도구 생태계까지 시리즈의 전체 로드맵을 제시합니다.
Python 3.12의 PEP 701 유연한 f-string 파싱과 3.12~3.13의 에러 메시지 개선을 다룹니다. 컬러 트레이스백, 제안 기반 에러 등 디버깅 경험 향상을 살펴봅니다.