Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 821 – 호출 가능 객체(Callable) 타입 힌트에서 TypedDict 언패킹 지원

Author:
Daniel Sperber <github.blurry at 9ox.net>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
12-Jan-2026
Python-Version:
3.15
Post-History:
28-Jun-2025, 31-Jan-2026

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 Callable 내부의 매개변수 목록에 Unpack[TypedDict]를 허용할 것을 제안합니다. 이를 통해 키워드 전용 호출 가능 객체의 시그니처를 간결하고 타입 안전하게 기술할 수 있습니다. 현재 Callable은 위치 전용 매개변수를 가정하므로, 키워드 전용 함수에 타입을 지정하려면 장황한 콜백 프로토콜을 작성해야 합니다. 이 제안을 통해 TypedDict로 정의한 키워드 구조를 Callable에서 직접 재사용할 수 있습니다.

동기

타입 명세에는 다음과 같이 명시되어 있습니다.

“호출 가능 객체(Callable)를 사용하여 지정한 매개변수는 위치 전용으로 간주됩니다. 호출 가능 객체(Callable) 형식으로는 키워드 전용 매개변수나 기본 인자 값을 지정할 방법이 없습니다. 이러한 사용 사례에 대해서는 콜백 프로토콜 절을 참고하십시오.”

이 제약 때문에 키워드 인자로 호출하도록 의도된 호출 가능 객체를 선언하기가 번거롭습니다. 기존 해결책은 Protocol을 정의하는 것입니다:

class KeywordTD(TypedDict, closed=True):
    a: int

class KwCallable(Protocol):
    def __call__(self, **kwargs: Unpack[KeywordTD]) -> Any: ...

# or

class KwCallable(Protocol):
    def __call__(self, *, a: int) -> Any: ...

이 방법은 동작하지만 장황합니다. 새 문법을 사용하면 같은 내용을 더 간결하게 작성할 수 있습니다:

type KwCallable = Callable[[Unpack[KeywordTD]], Any]

근거

설계 목표

주요 목표는 “특정 키워드 인자로 호출되도록 의도된 콜백”이라는 흔한 패턴을 Callable로 간단하게 표현할 수 있도록 하는 것입니다. 현재 이러한 콜백은 **kwargs: Unpack[...]를 사용하거나 각 키워드 매개변수를 명시적으로 포함하는 __call__을 가진 Protocol로 작성해야 합니다. 이 방식은 장황하며 위치 가변 인자를 지원하는 방식과도 일관되지 않습니다. PEP 646에 따르면 *args는 Callable 내부에서 *tuple[int, ...]로 표현할 수 있습니다.

Callable 내부에 Unpack[TypedDict]를 허용하면 다음과 같은 효과를 얻을 수 있습니다.

  • 익숙한 Callable[[...], R] 형태를 유지하면서 키워드 전용 매개변수를 기술할 수 있게 합니다.
  • __call__(self, **kwargs: Unpack[TD]) -> R을 사용하는 프로토콜(Protocol) 기반 콜백과 동등한 간결한 축약 표현을 제공합니다.
  • 위치 가변 인자에 대응하는 직관적인 표현을 제공합니다.
  • 기존 구성 요소를 재사용하며 의미론을 예측 가능하게 유지합니다. 이는 PEP 692 (**kwargs에 대한 Unpack) 및 PEP 728 (extra_items와 closed)의 기존 의미론과 일치합니다.
  • 기능을 추가적이고 하위 호환성 있게 유지하며, 대부분 타입 명세의 변경에 그칩니다.

검토한 대안

  1. Protocol 기반 콜백만 계속 권장합니다. 이렇게 하면 현 상태가 유지되고 Callable을 변경하지 않아도 됩니다. 그러나 문법적으로 더 무겁고, Callable에 이미 존재하는 개념을 중복합니다.
  2. 키워드를 위한 새로운 Callable 문법을 도입합니다(예: Callable 내부에 전용 키워드 매개변수 마커를 둡니다). 이렇게 하려면 새로운 구성 요소로 호출 가능 객체의 매개변수 문법을 확장해야 하며, 선택성, 기본값, 추가 키워드에 대한 의미 체계를 새로 만들어야 합니다. 이 설계 공간은 TypedDict 및 PEP 692와 겹치며, 기존 **kwargs 타이핑과 다르게 동작할 위험이 있습니다.
  3. 풍부한 시그니처를 인라인으로 표현하기 위해 콜백 리터럴 문법(PEP 677 참고)을 채택합니다. 리터럴 문법은 가독성을 높일 수 있지만, 더 크고 별개인 변경을 도입합니다. 이 PEP는 Callable 및 기존 타이핑 의미 체계 안에서 동작하는, 범위가 좁은 최소한의 확장을 목표로 합니다.

설계상의 절충과 결정

  • 위치 매개변수: 위치 매개변수가 Unpack[TD] 앞에 오도록 허용하면 기존 Callable 의미가 유지되며, 위치 매개변수와 키워드 전용 매개변수가 공존하는 실제 Python 함수의 형태를 그대로 반영합니다.
  • Concatenate: Unpack[TD]를 Concatenate와 결합하면 *args와 **kwargs 사이에 키워드 전용 매개변수를 끼워 넣을 수 있게 됩니다. 이는 복잡성을 높이므로 여기서는 제안하지 않습니다.

명세

새로 허용되는 형태

다음과 같이 작성하는 것이 유효해집니다:

Callable[[Unpack[TD]], R]

여기서 TD는 TypedDict입니다. 더 짧은 형태도 허용됩니다:

Callable[Unpack[TD], R]

또한 위치 매개변수를 언패킹된 TypedDict와 결합할 수 있습니다:

Callable[[int, str, Unpack[TD]], R]

의미론

타입 검사에서 Callable[[Unpack[TD]], R]은 __call__ 메서드가 **kwargs: Unpack[TD]를 갖는 콜백 프로토콜로 지정된 것처럼 동작합니다. Unpack 자체의 의미 체계는 타이핑 명세의 Unpack for keyword arguments 절과 PEP 692에 설명된 것과 정확히 같으며, extra_items 및 closed에 대해서는 PEP 728도 함께 적용됩니다.

이 PEP는 Callable(호출 가능 객체)에만 적용되는 다음 규칙만 추가합니다:

  • Unpack[TD]는 Callable의 매개변수 목록 안에 나타날 수 있습니다.
  • 위치 매개변수는 Callable 안에서 Unpack[TD] 앞에 나타날 수 있으며, 기존 Callable 의미 체계를 따릅니다.
  • Callable 안에서 언패킹된 TypedDict로 대체될 수 있는 것은 ParamSpec뿐입니다.

예시

다음 예제는 TypedDict를 Callable에 언패킹하면 특정 키워드 매개변수를 반드시 받아들이도록 강제된다는 것을 보여 줍니다. 함수는 필수 키워드로 호출될 수 있으면 호환됩니다(해당 키워드를 위치 인자로도 받는 경우 포함). 그러나 해당 키에 대한 위치 전용 매개변수는 거부됩니다:

from typing import TypedDict, Callable, Unpack, Any, NotRequired

class KeywordTD(TypedDict):
    a: int

type IntKwCallable = Callable[[Unpack[KeywordTD]], Any]

def normal(a: int): ...
def kw_only(*, a: int): ...
def pos_only(a: int, /): ...
def different(bar: int): ...

f1: IntKwCallable = normal     # Accepted
f2: IntKwCallable = kw_only    # Accepted
f3: IntKwCallable = pos_only   # Rejected
f4: IntKwCallable = different  # Rejected

선택적 인자

TypedDict에서 NotRequired로 표시된 키는 선택적 키워드 인자에 대응합니다. 즉, 호출 가능 객체는 이를 반드시 받아들여야 하지만, 호출자는 이를 생략할 수 있습니다. 키워드 인자를 받아들이는 함수는 호환되는 기본값도 제공해야 하며, 매개변수를 아예 생략한 함수는 거부됩니다:

class OptionalKws(TypedDict):
    a: NotRequired[int]

type OptCallable = Callable[[Unpack[OptionalKws]], Any]

def defaulted(a: int = 1): ...
def kw_default(*, a: int = 1): ...
def no_params(): ...
def required(a: int): ...

g1: OptCallable = defaulted    # Accepted
g2: OptCallable = kw_default   # Accepted
g3: OptCallable = no_params    # Rejected
g4: OptCallable = required     # Rejected

추가 키워드 인자

기본 동작(extra_items 또는 closed 없음)

TypedDict가 extra_items나 closed를 지정하지 않으면 추가 키워드 인자가 object 타입으로 허용됩니다. 이것이 기본 동작입니다:

# implies extra_items=object
class DefaultTD(TypedDict):
  a: int

type DefaultCallable = Callable[[Unpack[DefaultTD]], Any]

def v_any(**kwargs: object): ...
def v_ints(a: int, b: int=2): ...

d1: DefaultCallable = v_any   # Accepted (implicit object for extras)
d1(a=1, c="more")             # Accepted (extras allowed)
d2: DefaultCallable = v_ints  # Rejected (b: int is not a supertype of object)

closed 동작 (PEP 728)

TypedDict에 closed=True가 지정되면 선언된 것 외의 추가 키워드 인자는 기대되지 않습니다:

class ClosedTD(TypedDict, closed=True):
  a: int

type ClosedCallable = Callable[[Unpack[ClosedTD]], Any]

def v_any(**kwargs: object): ...
def v_ints(a: int, b: int=2): ...

c1: ClosedCallable = v_any   # Accepted
c1(a=1, c="more")            # Rejected (extra c not allowed)
c2: ClosedCallable = v_ints  # Accepted
c2(a=1, b=2)                 # Rejected (extra b not allowed)

extra_items와의 상호작용 (PEP 728)

TypedDict가 extra_items 매개변수를 지정하면 (단, extra_items=Never는 예외), 대응하는 Callable은 지정된 타입의 추가 키워드 인자를 받아야 합니다.

예를 들면 다음과 같습니다:

class ExtraTD(TypedDict, extra_items=str):
  a: int

type ExtraCallable = Callable[[Unpack[ExtraTD]], Any]

def accepts_str(**kwargs: str): ...
def accepts_object(**kwargs: object): ...
def accepts_int(**kwargs: int): ...

e1: ExtraCallable = accepts_str     # Accepted (matches extra_items type)
e2: ExtraCallable = accepts_object  # Accepted (object is a supertype of str)
e3: ExtraCallable = accepts_int     # Rejected (int is not a supertype of str)

e1(a=1, b="foo")   # Accepted
e1(a=1, b=2)       # Rejected (b must be str)

ParamSpec 및 Concatenate와의 상호작용

ParamSpec을 Unpack[KeywordTD]로 치환하여 매개변수화된 호출 가능 객체 별칭을 정의할 수 있습니다. Unpack[KeywordTD]로 치환하면 언팩된 TypedDict로 호출 가능 객체를 직접 작성한 것과 같은 효과가 납니다. Concatenate 안에서 TypedDict를 사용하는 것은 허용되지 않습니다.

type CallableP[**P] = Callable[P, Any]

h: CallableP[Unpack[KeywordTD]] = normal   # Accepted
h2: CallableP[Unpack[KeywordTD]] = kw_only # Accepted
h3: CallableP[Unpack[KeywordTD]] = pos_only # Rejected

추가 괄호 없이 제네릭 Unpack[TypedDict]로 첨자를 지정할 수 있도록 현재 구현을 갱신해야 합니다. Backwards Compatibility를 참조하십시오.

위치 매개변수와 Unpack의 결합

Callable 안에서 언팩된 TypedDict 앞에 위치 매개변수가 올 수 있습니다. 필요한 위치 인자를 받고 지정된 키워드(들)로 호출될 수 있는 함수는 호환되며, 해당 키워드를 위치 전용으로 만드는 것은 거부됩니다:

from typing import TypedDict, Callable, Unpack, Any

class KeywordTD(TypedDict):
    a: int

type IntKwPosCallable = Callable[[int, str, Unpack[KeywordTD]], Any]

def mixed_kwonly(x: int, y: str, *, a: int): ...
def mixed_poskw(x: int, y: str, a: int): ...
def mixed_posonly(x: int, y: str, a: int, /): ...

m1: IntKwPosCallable = mixed_kwonly  # Accepted
m2: IntKwPosCallable = mixed_poskw   # Accepted
m3: IntKwPosCallable = mixed_posonly # Rejected

하위 호환성

이 기능은 대체로 타입 지정에만 관련된 부가적인 기능입니다. 기존 코드에는 영향을 주지 않습니다. TypedDict의 제네릭 Unpack으로 ParamSpec에 첨자를 지정하는 것은 추가 괄호 안에 넣을 때만 하위 호환되며, TypeAliasType은 이에 영향을 받지 않습니다:

from typing import TypedDict, ParamSpec, Callable, Unpack
from typing import TypeAliasType
class Config[T](TypedDict):
    setting: T

type CallP1[**P] = Callable[P, None]
CallP1_SubbedB = CallP1[Unpack[Config[int]]]    # OK

P = ParamSpec("P")
CallP2 = Callable[P, None]
CallP2_SubbedA = CallP2[[Unpack[Config[int]]]]  # OK
CallP2_SubbedB = CallP2[Unpack[Config[int]]]    # currently TypeError

교육 방법

이 기능은 프로토콜 기반 콜백의 축약 표현입니다. 사용자에게는 다음 내용을 가르쳐야 합니다

class KeywordTD(TypedDict):
    a: int
    b: NotRequired[str]
  • Callable[[Unpack[KeywordTD]], R]은 __call__(self, **kwargs: Unpack[KeywordTD]) -> R 또는 __call__(self, a: int, b: str = ..., **kwargs: object) -> R을 가진 프로토콜을 정의하는 것과 같습니다.
  • 교육자는 Protocol을 소개하기 전에 Callable과 함께 TypedDict 개념을 먼저 소개하는 것을 고려할 수 있습니다.
  • **kwargs: object가 암묵적으로 추가되는 것은 사용자에게 뜻밖일 수 있습니다. 정의에 closed=True를 사용하면 __call__(self, a: int, b: str = ...) -> R과 더 직관적으로 동등해집니다
  • PEP 728의 extra_items와의 상호작용을 사용자에게 알려야 합니다.

참조 구현

mypy에 프로토타입이 있습니다: python/mypy#16083.

기각된 아이디어

  • Unpack[TD]와 Concatenate의 결합. 이를 지원하면 Callable[Concatenate[int, Unpack[TD], P], R]와 같이 작성할 수 있고, 이는 다시 *args와 **kwargs 사이에 키워드 전용 매개변수를 허용하게 됩니다. 즉, def func(*args: Any, a: int, **kwargs: Any) -> R: ...와 같은 형태이며, 이는 현재 PEP 612에 따라 허용되지 않습니다. 초기 구현을 단순하게 유지하기 위해 이 PEP는 그러한 지원을 제안하지 않습니다.

미해결 질문

  • 여러 TypedDict 언패킹으로 유니온을 구성하도록 허용해야 합니까? 허용한다면 타입이 서로 다른 겹치는 키는 어떻게 처리해야 합니까? 그러한 경우에는 어떤 제한을 적용해야 합니까? 순서에 따라 결과가 달라져야 합니까?
  • Callable[[Unpack[TD]], R]뿐만 아니라 더 짧은 형태인 Callable[Unpack[TD], R]도 허용해야 합니까?
  • 일반 키와 ReadOnly 키를 구별할 필요가 있습니까?

감사의 글

이 PEP를 후원하고 귀중한 검토 피드백을 주신 Jelle Zijlstra에게 감사드립니다.

이 PEP의 초안과 PR에 유익한 피드백을 주신 Hugo van Kemenade에게 감사드립니다.

초기 아이디어와 논의에 피드백을 주신 Eric Traut에게 감사드립니다.

참고 문헌