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

Python 개선 제안 한국어 번역

PEP 827 – 타입 조작

Author:
Michael J. Sullivan <sully at vercel.com>, Daniel W. Park <daniel.park at vercel.com>, Yury Selivanov <yury at vercel.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
27-Feb-2026
Python-Version:
3.16
Post-History:
02-Mar-2026

Table of Contents

번역·라이선스 안내

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

초록

Python의 타입 시스템에 강력한 타입 수준 인트로스펙션 및 구성 기능을 추가할 것을 제안합니다. 이 설계는 주로 TypeScript의 조건부 타입과 매핑된 타입에서 영감을 받았지만, Python 타이핑 모델의 고유한 의미론과 제약에 맞게 조정되었습니다.

큰 그림에서 보면, 이 제안의 목표는 다음과 같습니다:

  • typing 모듈에 새로운 프리미티브를 도입합니다;
  • Python 문법을 변경하지 않고도 정적 타입 검사기가 지원하는 어노테이션 구문을 확장합니다;
  • 새로운 타입 수준 메커니즘이 정적 타입 검사기와 런타임 타입 인트로스펙션에 의존하는 프레임워크 모두에 이점을 주도록 보장합니다.

동기

Python은 점진적 타입 시스템을 갖추고 있지만, 그 핵심에는 상당히 전통적인 정적 타입 시스템이 있습니다.

반면 언어로서의 Python에서는 특히 라이브러리와 프레임워크에서 복잡한 메타프로그래밍을 수행하는 일이 드물지 않습니다. 타입 시스템은 일반적으로 메타프로그래밍을 모델링할 수 없습니다.

메타프로그래밍과 타입 시스템 사이의 간극을 메우기 위해, 일부 라이브러리는 자체 mypy 플러그인을 함께 제공합니다. 데이터클래스와 유사한 변환은 충분히 흔한 경우로 여겨져, 이 경우만을 다루기 위한 특수 목적의 @dataclass_transform 데코레이터가 추가되었습니다(PEP 681). 이 접근 방식의 문제는 많은 타입 검사기가 플러그인 API를 갖추고 있지 않으며 앞으로도 갖추지 않을 것이라는 점으로, 따라서 IDE, CI, 도구 전반에 걸쳐 일관된 타입 검사를 달성할 수 없습니다.

Python 언어의 표현력과 그 타입 시스템 사이에 상당한 불일치가 있다는 점을 고려하여, 동적인 Python 코드를 더 잘 따라갈 수 있는 타입 조작 기능을 추가함으로써 이 간극을 메울 것을 제안합니다.

이에 대한 수요가 있습니다. Meta의 2025 Typed Python Survey 응답 분석에서 “가장 많이 요청된 기능” 목록의 첫 번째 항목은 다음과 같았습니다:

TypeScript 및 기타 언어 대비 부족한 기능: 많은 응답자가 교차 타입(& 연산자 등), 매핑된 타입과 조건부 타입, 유틸리티 타입(Pick, Omit, keyof, typeof 등), 그리고 딕셔너리/dict를 위한 더 나은 구조적 타이핑(예: 더 유연한 TypedDict 또는 익명 타입)과 같이 TypeScript에서 영감을 받은 기능을 요청했습니다.

더 강력한 타입 조작으로 해결할 수 있는 문제의 예를 몇 가지 제시하겠지만, 이 제안은 범용적이며 훨씬 더 많은 사용 사례를 가능하게 할 것입니다.

Prisma 스타일 ORM

TypeScript용으로 널리 쓰이는 ORM인 Prisma를 사용하면 TypeScript로 다음과 같이 데이터베이스 쿼리를 작성할 수 있습니다(이 예제를 변형함):

const user = await prisma.user.findMany({
  select: {
    name: true,
    email: true,
    posts: true,
  },
});

이때 user의 추론된 타입은 대략 다음과 같습니다:

{
    email: string;
    name: string | null;
    posts: {
        id: number;
        title: string;
        content: string | null;
        authorId: number | null;
    }[];
}[]

여기서 출력 타입은 prisma.user의 타입(데이터베이스 user 테이블을 반영한 TypeScript 타입)에 관한 기존 정보와 findMany() 메서드에 전달된 인자의 타입을 교차한 것입니다. 이는 명시적으로 요청된 user의 속성들을 담은 객체의 배열을 반환하며, 여기서 posts는 다른 타입을 참조하는 “관계(relation)”입니다.

Python에서도 이와 비슷한 일을 할 수 있기를 바랍니다. 데이터베이스 스키마가 Python으로(또는 데이터베이스로부터 코드 생성되어) 다음과 같이 정의되어 있다고 가정해 봅시다:

class Comment:
    id: Property[int]
    name: Property[str]
    poster: Link[User]


class Post:
    id: Property[int]

    title: Property[str]
    content: Property[str]

    comments: MultiLink[Comment]
    author: Link[User]


class User:
    id: Property[int]

    name: Property[str]
    age: Property[int | None]
    email: Property[str]
    posts: Link[Post]

(예제에서 Property는 스칼라 타입을, Link는 다른 테이블에 대한 참조를, MultiLink는 다른 테이블에 대한 다대다일 수 있는 참조를 나타내며, 이들은 모두 ORM 라이브러리가 정의합니다.)

그러면 Python 코드에서 다음과 같은 호출은:

db.select(
    User,
    name=True,
    email=True,
    posts=True,
)

동적으로 계산된 반환 타입 list[<User>]를 가지게 되며, 여기서:

class <User>:
    name: str
    email: str
    posts: list[<Post>]

class <Post>:
    id: int
    title: str
    content: str

더 나아가 IDE는 db.select() 호출의 모든 인자에 대해 (실제 데이터베이스 열 이름과 일치하는) 코드 완성을 재귀적으로 제공할 수 있습니다.

(이를 구현하는 예제 코드는 아래에 있습니다.)

FastAPI CRUD 모델 자동 파생

FastAPI 튜토리얼은 간단한 Hero 타입에 대한 CRUD 엔드포인트를 만드는 방법을 보여 줍니다. 그 핵심에는 일련의 클래스 정의가 있으며, 이 클래스들은 데이터베이스 인터페이스를 정의하는 데에도, 엔드포인트에서 데이터를 검증하고 필터링하는 데에도 사용됩니다:

class HeroBase(SQLModel):
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)


class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str


class HeroPublic(HeroBase):
    id: int


class HeroCreate(HeroBase):
    secret_name: str


class HeroUpdate(HeroBase):
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None

HeroPublic 타입은 읽기 엔드포인트의 반환 타입으로 사용되며(출력되는 동안 검증되고, 추가 필드도 제거됩니다), HeroCreate와 HeroUpdate는 입력 타입으로 쓰입니다(Pydantic을 사용하여 JSON에서 자동으로 변환되고 타입에 따라 검증됩니다).

여기에는 여러 타입과 중복이 있지만, 이 타입들을 파생하는 기계적인 규칙을 작성할 수 있습니다:

  • “Public” 버전은 “hidden”이 아닌 모든 필드를 포함해야 하며, 기본 키는 옵셔널이 아니도록 만들어야 합니다
  • “Create”는 기본 키를 제외한 모든 필드를 포함해야 합니다
  • “Update”는 기본 키를 제외한 모든 필드를 포함해야 하지만, 이 필드들은 모두 옵셔널로 만들고 기본값을 지정해야 합니다

FastAPI 프레임워크 내부에 적절한 헬퍼를 정의하면, 이 제안을 통해 사용자는 다음과 같이 작성할 수 있습니다:

class Hero(NewSQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)

    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)

    secret_name: str = Field(hidden=True)

type HeroPublic = Public[Hero]
type HeroCreate = Create[Hero]
type HeroUpdate = Update[Hero]

이 타입들을 평가하면 대략 다음과 같은 모습이 됩니다:

class HeroPublic:
    id: int
    name: str
    age: int | None


class HeroCreate:
    name: str
    age: int | None = None
    secret_name: str


class HeroUpdate:
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None

Public[], Create[], Update[] 계산된 타입의 구현은 비교적 복잡하지만, 이들은 상당히 기계적인 연산을 수행하므로 프레임워크 라이브러리에 포함된다면 FastAPI 사용자가 유지 관리해야 하는 보일러플레이트를 크게 줄여 줄 것입니다.

이 사용 사례의 주목할 만한 특징은 타입 어노테이션의 런타임 평가를 수행해야 한다는 점입니다. FastAPI는 엔드포인트의 입력과 출력 모두에 대해 Pydantic 모델을 사용하여 검증하고 JSON과 상호 변환합니다.

현재도 이 중 런타임 쪽 절반은 가능합니다: 원하는 규칙에 따라 런타임에 Pydantic 모델을 생성하는 함수를 작성할 수 있습니다. 하지만 이 방식은 만족스럽지 않은데, 그 함수들을 제대로 정적 타입 검사할 수 없기 때문입니다.

(이를 구현하는 예제 코드는 아래에 있습니다.)

dataclasses 스타일의 메서드 생성

또한 객체의 속성을 바탕으로 메서드 시그니처를 생성할 수 있기를 바랍니다. 가장 잘 알려진 예는 데이터클래스의 __init__ 메서드를 생성하는 것이며, 여기서는 이를 단순화한 예를 제시합니다.

이러한 패턴은 매우 널리 퍼져 있어서, 기존 라이브러리들이 하는 일의 최소 공통분모 부분 집합을 표현하기 위해 PEP 681이 만들어졌을 정도입니다.

라이브러리가 이러한 패턴을 타입 시스템에서 더 많이 직접 구현할 수 있게 하면, 추가적인 특수 처리, 타입 검사기 플러그인, 하드코딩된 지원 등이 없어도 더 나은 타이핑을 제공할 수 있습니다.

(이를 구현하는 예제 코드는 아래에 있습니다.)

더 강력한 데코레이터 타이핑

데코레이터 함수의 타입 지정은 오랫동안 Python 타이핑의 골칫거리였습니다. PEP 612에서 ParamSpec이 도입되면서 상황이 크게 개선되었지만, 여전히 지원되지 않는 패턴이 많이 남아 있습니다:

  • 키워드 매개변수 추가/제거/수정.
  • 가변 개수의 매개변수 추가/제거/수정. (다만 여러 번의 언패킹이 허용된다면 TypeVarTuple로 추가와 제거를 거의 지원할 수 있으며, Pyre는 여러 개를 수정할 수 있는 Map 연산자를 구현한 바 있습니다.)

이 제안은 이러한 경우들을 다룹니다.

몇 가지 전제 조건의 명세

이 제안의 주요 부분을 제대로 활용하려면 두 가지 하위 제안이 필요합니다.

**kwargs를 위한 타입 변수 언팩

PEP 없이 타이핑 제안으로 분리할 수 있을 것으로 보이는 작은 제안입니다:

**kwargs에 대한 타입 변수 Unpack 지원:

def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K:
    return kwargs

여기서 BaseTypedDict는 다음과 같이 정의됩니다:

class BaseTypedDict(typing.TypedDict):
    pass

하지만 그 자리에는 어떤 TypedDict든 허용됩니다.

그러면 다음과 같은 호출이 있을 때:

x: int
y: list[str]
f(x=x, y=y)

K에 대해 추론되는 타입은 대략 다음과 같습니다:

TypedDict({'x': int, 'y': list[str]})

이는 기본적으로 PEP 692 “Using TypedDict for more precise **kwargs typing”과 PEP 646 “Variadic Generics”의 *args에 대한 Unpack 동작을 결합한 것입니다.

여기서 타입을 추론할 때 타입 검사기는 가능한 경우 리터럴 타입을 추론해야 합니다. 이는 바운드에 나타나지 않는 인자뿐만 아니라, 바운드에 읽기 전용으로 나타나는 인자에 대해서도 리터럴 타입을 추론한다는 뜻입니다.

바운드에서 필수가 아닌 각 항목 중 일치하는 인자가 제공되지 않은 항목에 대해, 그 항목이 읽기 전용이면 제공되지 않았음을 나타내기 위해 그 타입은 Never로 추론됩니다. (이는 읽기 전용 항목에 대해서만 가능한데, 읽기 전용이 아닌 항목은 불변 조건을 따르기 때문입니다.)

이는 그 자체로도 어느 정도 유용할 수 있지만, 타입 수준 계산으로 **kwargs를 처리하는 것을 지원하기 위해 도입됩니다.

확장 호출 가능 객체

임의로 복잡한 호출 가능 객체 타입을 표현하기 위한 새로운 확장 호출 가능 객체 제안을 도입합니다. 여기서의 목표는 어노테이션에 작성할 새로운 문법을 만드는 것이 아니라 (그러기에는 상당히 장황합니다), 이 PEP의 다른 기능을 사용하여 호출 가능 객체 타입을 생성하고 인트로스펙션하기에 적합한 타입 구성 방법을 제공하는 것입니다.

함수 매개변수에 대한 모든 정보를 담는 Param 타입을 도입합니다:

class Param[
    N: str | None,
    T,
    K: ParamKind = Literal[ParamKind.POSITIONAL_OR_KEYWORD],
    D = typing.Never,
]:
    pass

class ParamKind(enum.IntEnum):
    POSITIONAL_ONLY = 0
    POSITIONAL_OR_KEYWORD = 1
    VAR_POSITIONAL = 2
    KEYWORD_ONLY = 3
    VAR_KEYWORD = 4

type PosParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY]]
type PosDefaultParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY], T]
type DefaultParam[N: str, T] = Param[N, T, Literal[ParamKind.POSITIONAL_OR_KEYWORD], T]
type NamedParam[N: str, T] = Param[N, T, Literal[ParamKind.KEYWORD_ONLY]]
type NamedDefaultParam[N: str, T] = Param[N, T, Literal[ParamKind.KEYWORD_ONLY], T]
type ArgsParam[T] = Param[None, T, Literal[ParamKind.VAR_POSITIONAL]]
type KwargsParam[T] = Param[None, T, Literal[ParamKind.VAR_KEYWORD]]

ParamKind 타입의 인자 K는 매개변수의 종류를 나타내며, 기본값은 일반적인 Literal[ParamKind.POSITIONAL_OR_KEYWORD]입니다. ParamKind는 inspect._ParameterKind를 그대로 반영합니다. 여러 종류가 유니언으로 결합된 Param으로 Callable을 생성하는 것은 오류입니다.

인자 D는 매개변수에 기본값이 있으면 그 기본값의 타입을 담고, 그렇지 않으면 Never입니다. 기본값이 리터럴(예: None, int, 문자열, enum 멤버)인 경우, D는 그 값을 담는 Literal일 수 있습니다. (이러한 Literal로 두는 것은 해당 값을 인트로스펙션에서, 그리고 경우에 따라 진단에서 사용할 수 있게 하는 것 외에는 아무런 효과가 없습니다.)

또한 Param 타입의 시퀀스를 감싸며 Callable의 첫 번째 인자로 쓰이는 Params 타입을 도입합니다:

class Params[*Ps]:
    pass

Callable의 첫 번째 인자로 Params가 있으면 확장 호출 가능 객체 형식이 표준 형식과 구별됩니다. Params는 ParamSpec의 자연스러운 바운드 역할도 합니다.

그러면 다음과 같은 함수의 타입을:

def func(
    a: int,
    /,
    b: int,
    c: int = 0,
    *args: int,
    d: int,
    e: int = 0,
    **kwargs: int
) -> int:
    ...

다음과 같이 나타낼 수 있습니다:

Callable[
    Params[
        Param[Literal["a"], int, Literal[ParamKind.POSITIONAL_ONLY]],
        Param[Literal["b"], int],
        Param[Literal["c"], int, Literal[ParamKind.POSITIONAL_OR_KEYWORD], Literal[0]],
        Param[None, int, Literal[ParamKind.VAR_POSITIONAL]],
        Param[Literal["d"], int, Literal[ParamKind.KEYWORD_ONLY]],
        Param[Literal["e"], int, Literal[ParamKind.KEYWORD_ONLY], Literal[0]],
        Param[None, int, Literal[ParamKind.VAR_KEYWORD]],
    ],
    int,
]

또는 우리가 제공하는 타입 약어를 사용하여 다음과 같이 나타낼 수 있습니다(다만 이 버전은 기본값의 구체적인 값을 추적하지 않습니다):

Callable[
    Params[
        PosParam[Literal["a"], int],
        Param[Literal["b"], int],
        DefaultParam[Literal["c"], int],
        ArgsParam[int],
        NamedParam[Literal["d"], int],
        NamedDefaultParam[Literal["e"], int],
        KwargsParam[int],
    ],
    int,
]

(근거는 아래에서 논의합니다.)

명세

위 예제에서 보았듯이 유효한 타입의 새로운 문법 형식을 몇 가지 도입하지만, 강력함의 대부분은 typing 모듈에 정의될 타입 수준 연산자에서 나옵니다.

타입 언어 확장의 문법 명세

Python 문법에 대한 변경은 제안하지 않으며, 어떤 Python 표현식이 유효한 타입으로 간주되는지에 대한 문법만 변경합니다.

<type> = ...
     # Type booleans are all valid types too
     | <type-bool>

     # Conditional types
     | <type> if <type-bool> else <type>

     # Types with variadic arguments can have
     # *[... for t in ...] arguments
     | <ident>[<variadic-type-arg> +]

     # Type member access
     | <type>.<name>

     | GenericCallable[<type>, lambda <args>: <type>]

# Type conditional checks are boolean compositions of
# boolean type operators
<type-bool> =
      <bool-operator>[<type> +]
    | not <type-bool>
    | <type-bool> and <type-bool>
    | <type-bool> or <type-bool>
    | any(<type-bool-for>)
    | all(<type-bool-for>)

<variadic-type-arg> =
      <type> ,
    | * [ <type-for-iter> ] ,


<type-for> = <type> <type-for-iter>+ <type-for-if>*
<type-for-iter> =
      # Iterate over a tuple type
      for <var> in Iter[<type>]
<type-for-if> =
      if <type-bool>

여기서:

  • <bool-operator>는 불리언 연산자 절에 정의된 이름 중 어느 것이든 가리키며, 직접 사용하든 한정하여 사용하든 다른 이름으로 사용하든 마찬가지입니다.
  • <type-bool-for>는 결과 타입이 <type>이 아니라 <type-bool>이라는 점을 제외하면 <type-for>와 동일합니다.

세 가지 반의 핵심 문법 기능을 도입합니다. 타입 불리언, 조건부 타입, 언패킹된 컴프리헨션 타입, 그리고 타입 멤버 접근입니다.

“제네릭 호출 가능 객체”도 엄밀히 말하면 구문 기능이지만, 연산자로 다룹니다.

타입 불리언

타입 불리언은 조건문의 본문에서 사용할 수 있는 타입 언어의 특수한 부분집합입니다. 타입 불리언은 아래에서 정의하는 불리언 연산자로 구성되며, and, or, not, all, any와 조합될 수도 있습니다. all과 any의 경우, 인자는 타입 불리언의 컴프리헨션이며, 언팩된 컴프리헨션과 같은 방식으로 평가됩니다.

타입 어노테이션 문맥에서 평가될 때, 타입 불리언은 Literal[True] 또는 Literal[False]와 동등합니다.

조건문에서 사용할 수 있는 연산자를 제한하는 이유는, 기존 Literal[False] 값 등의 동작을 바꿀 필요 없이 런타임에 해당 연산자들이 적절한 동작을 하는 “타입” 값을 만들어 내도록 하기 위해서입니다.

조건부 타입

true_typ if bool_typ else false_typ 타입은 조건부 타입으로, bool_typ이 Literal[True]와 동등하면 true_typ으로, 그렇지 않으면 false_typ으로 결정됩니다.

bool_typ은 타입이지만, 구문상으로는 위에서 정의한 타입 불리언이어야 합니다.

언팩된 컴프리헨션

언팩된 컴프리헨션 *[ty for t in Iter[iter_ty]]는 현재 Unpack[...]이 허용되는 타입 내 어느 위치에나 나타날 수 있으며, 본질적으로 튜플 타입 iter_ty의 인자들을 순회하는 리스트 컴프리헨션이 만들어 낸 튜플의 Unpack으로 평가됩니다.

컴프리헨션에는 if 절도 올 수 있으며, 이 절은 일반적인 방식으로 필터링합니다.

타입 멤버 접근

클래스 멤버와 함수 매개변수를 표현하기 위해 도입된 Member 및 Param 타입에는 “연관된” 타입 멤버가 있으며, 이는 점 표기법(m.name, m.type 등)으로 접근할 수 있습니다.

이 연산은 유니온 타입에 대해 리프팅되지 않습니다. 잘못된 종류의 타입에 이 연산을 사용하면 오류가 발생합니다. 런타임에서 반드시 그렇게 동작해야 하며, 타입 검사도 이와 일치하도록 하고자 합니다.

타입 연산자

타입 연산자는 타입 조작의 핵심 엔진이며, 타입을 분해하고 새로운 타입을 구성하는 데 사용되는 기본 요소를 제공합니다.

이 절에서는 도입되는 연산자들을 정의하고, 이 연산자들이 타입 검사 또는 타입 평가 문맥에서 어떻게 평가되어야 하는지 설명합니다. 하지만 실제로 도입되는 런타임 클래스는 일반 클래스일 뿐이며, 이를 서브스크립트하면 일반적인 typing 제네릭 별칭 객체가 만들어집니다(불리언 연산자와 Iter는 부분적인 예외로, 런타임 훅을 위해 일부 던더 메서드가 오버로드된 별칭을 만들어 냅니다).

명시된 연산자 중 상당수에는 일부 피연산자에 대한 타입 경계가 나열되어 있습니다. 이러한 경계는 정확한 타입 경계라기보다는 문서로서 해석해야 합니다. 유효하지 않은 인자로 연산자를 평가하려고 하면 오류가 발생합니다. 이 경우 실패한 연산자의 값은 Any가 되므로, 이후의 평가에서 추가 오류가 연쇄적으로 발생하지 않습니다. (가능한 대안에 대한 논의가 아래에 일부 있습니다.)

아래의 일부 경계에서는 “int 타입인 리터럴”을 의미하기 위해 Literal[int]와 같은 표기를 사용한다는 점에 유의하십시오. 아직은 이를 실제 문법으로 추가하자고 제안하지 않습니다.

불리언 연산자

  • IsAssignable[T, S]: T가 S에 할당 가능한지를 나타내는 불리언 리터럴 타입을 반환합니다.

    즉, “일관된 서브타입”인지를 나타냅니다. 이는 서브타이핑을 점진적 타입으로 확장한 것입니다.

  • IsEquivalent[T, S]: IsAssignable[T, S] and IsAssignable[S, T]와 동등합니다. 엄밀히 말하면 이 관계는 동등성이 아니라 타이핑 명세에서 말하는 “일관성(consistency)”입니다.
  • Bool[T]: T도 Literal[True]이면 Literal[True]를 반환합니다. IsAssignable[T, Literal[True]] and not IsAssignable[T, Never]와 동등합니다.

    이는 불리언 리터럴 타입을 반환하는 “헬퍼 별칭”을 호출할 때 유용합니다.

기본 연산자

  • GetArg[T, Base, Idx: Literal[int]]: T를 Base로 해석했을 때의 Idx번째 타입 인자를 반환합니다. 그렇게 해석할 수 없거나 인덱스가 유효하지 않으면 타입 오류를 발생시킵니다. (즉, class A(B[C]): ...가 있다면 GetArg[A, B, Literal[0]] == C이지만 GetArg[A, A, Literal[0]]는 타입 오류입니다.)

    T가 Any이면 결과는 Any입니다.

    음수 인덱스는 일반적인 방식으로 동작합니다.

    (타입 어노테이션을 런타임에 평가하는 평가기는 프로토콜을 Base로 사용하는 경우를 처리하기 어려울 가능성이 높다는 점에 유의하십시오. 예를 들어 이터러블한 대상의 타입을 얻기 위한 GetArg[Ty, Iterable, Literal[0]]는 런타임 타입 평가기에서 실패할 수 있습니다.)

    특수 형식은 특별한 처리가 필요합니다. Callable의 인자 목록은 Params로 묶입니다. ...는 *args: Any와 **kwargs: Any로 취급되며, 이들은 새로운 Param 타입으로 표현됩니다.

  • GetArgs[T, Base]: T를 Base로 해석했을 때의 모든 타입 인자를 담은 튜플을 반환합니다. 그렇게 해석할 수 없으면 오류를 반환합니다.

    T가 Any이면 결과는 Any입니다.

  • Length[T: tuple] - 튜플의 길이를 int 리터럴로 가져옵니다(길이가 제한되지 않은 경우에는 Literal[None]입니다).
  • Slice[S: tuple, Start: Literal[int | None], End: Literal[int | None]]: 튜플 타입을 슬라이스합니다.

    S가 Any이면 결과는 Any입니다.

  • GetSpecialAttr[T, Attr: Literal[str]]: 클래스 T에서 Attr이라는 이름의 특수 속성 값을 추출합니다. 유효한 속성은 __name__, __module__, __qualname__입니다. 값을 Literal[str]로 반환합니다.

    클래스가 아닌 타입의 경우, x의 타입이 T이면 type(x).<attr>의 값을 얻는다는 것을 원칙으로 삼아야 합니다. 따라서 GetSpecialAttr[Literal[1], "__name__"]는 Literal["int"]를 만들어야 합니다.

이 절의 모든 연산자는 유니언 타입에 대해 리프팅됩니다.

유니언 처리

  • FromUnion[T]: 유니언의 모든 원소를 담은 튜플을 반환합니다. T가 유니언이 아니면 T 하나만 담은 1-튜플을 반환합니다.
  • Union[*Ts]: Union은 가변 인자를 받을 수 있게 되므로, 언패킹된 컴프리헨션 인자도 받을 수 있게 됩니다.

객체 검사

  • Members[T]: 클래스 또는 typed dict T의 멤버(속성과 메서드)를 설명하는 Member 타입들의 tuple을 생성합니다.

    타입 검사 시점의 평가와 런타임 평가가 더 밀접하게 일치하도록 명시적인 타입 어노테이션이 있는 멤버만 포함됩니다. (어노테이션이 없는 메서드도 제외하려는 의도이지만, 미해결 문제를 참고하십시오.)

  • Attrs[T]: Members[T]와 비슷하지만 속성만 반환합니다(메서드는 제외합니다).
  • GetMember[T, S: Literal[str]]: 클래스 T에서 이름이 S인 멤버에 대한 Member 타입을 만듭니다. 해당 멤버가 없으면 오류를 만듭니다.

    T가 Any이면 결과는 Any입니다.

  • GetMemberType[T, S: Literal[str]]: 클래스 T에서 이름이 S인 멤버의 타입을 추출하며, 해당 멤버가 존재하지 않으면 Never입니다.

    T가 Any이면 결과는 Any입니다.

  • Member[N: Literal[str], T, Q: MemberQuals, Init, D]: Member는 연산자가 아닌 단순한 타입으로, 클래스의 멤버를 기술하는 데 사용됩니다. 이 타입의 타입 매개변수는 각 멤버에 대한 정보를 인코딩합니다.
    • N은 리터럴 문자열 타입으로 표현된 이름입니다. .name으로 접근할 수 있습니다.
    • T는 타입입니다. .type으로 접근할 수 있습니다.
    • Q는 한정자들의 유니언입니다(아래의 MemberQuals 참조). .quals로 접근할 수 있습니다. 한정자가 없으면 Never입니다.
    • Init은 클래스에 있는 속성 초기화자의 리터럴 타입입니다(InitField 참고). .init으로 접근할 수 있습니다. 초기화자가 없으면 Never입니다.
    • D는 멤버를 정의한 클래스입니다. (즉, 멤버가 어느 클래스로부터 상속되었는지를 나타냅니다. TypedDict의 경우에는 항상 Never입니다). .definer로 접근할 수 있습니다.
  • MemberQuals = Literal['ClassVar', 'Final', 'NotRequired', 'ReadOnly'] - MemberQuals는 멤버에 적용될 수 있는 “한정자”의 타입입니다. 현재 ClassVar와 Final은 클래스에 적용되고, NotRequired와 ReadOnly는 typed dict에 적용됩니다.

메서드는 클래스 본문에 정의된 함수, 정적 메서드, 클래스 메서드입니다. 프로퍼티는 속성으로 취급해야 합니다.

메서드는 Param 기반 확장 호출 가능 객체로 인트로스펙션할 수 있고 ClassVar 한정자를 지닌 호출 가능 객체로 반환됩니다. staticmethod와 classmethod는 staticmethod 및 classmethod 타입을 반환하며, 이 타입들은 Python 3.14부터 첨자 표기가 가능합니다.

이 절의 모든 연산자는 유니언 타입에 대해 리프팅됩니다.

객체 생성

  • NewProtocol[*Ms: Member]: Member 인자로 지정된 멤버를 가진 새로운 구조적 프로토콜을 생성합니다
  • NewTypedDict[*Ps: Member] - Member 인자로 지정된 항목을 가진 새로운 TypedDict를 생성합니다.

현재로서는 명목적 클래스를 생성하는 방법이나 새로운 제네릭 타입을 만드는 방법은 제안하지 않는다는 점에 유의하십시오.

InitField

저희는 dataclasses/attrs/Pydantic 스타일의 필드 디스크립터를 기반으로 타입을 변환하는 기능을 지원할 수 있기를 원합니다. 이를 위해서는 Field 호출과 같은 연산을 처리할 수 있어야 합니다.

이를 위한 저희의 전략은 KwargDict: TypedDict로 정의된 인자를 수집하는 새로운 타입 InitField[KwargDict]를 도입하는 것입니다:

class InitField[KwargDict: BaseTypedDict]:
    def __init__(self, **kwargs: typing.Unpack[KwargDict]) -> None:
        ...

    def _get_kwargs(self) -> KwargDict:
        ...

InitField 또는 (더 가능성이 높게는) 그 서브타입이 클래스 본문 안에서 인스턴스화되면, 가능한 경우 Literal 타입을 기반으로 그에 대해 더 구체적인 타입을 추론합니다. (사실 이는 **kwargs에서의 타입 변수 언패킹이 Literal 타입을 사용해야 한다는 규칙을 적용한 것에 불과합니다.)

따라서 다음과 같이 작성하면:

class A:
    foo: int = InitField(default=0, kw_only=True)

초기화자에 대해 InitField[TypedDict('...', {'default': Literal[0], 'kw_only': Literal[True]})] 타입을 추론하게 되며, 이는 Member의 Init 필드로 사용할 수 있게 됩니다.

호출 가능 객체 검사 및 생성

Callable 타입의 인자는 항상 위에서 논의한 확장된 호출 가능 객체(Callable) 형식으로 노출됩니다.

Member의 이름과 타입에 대한 연관 타입 이름은 .name과 .type입니다. Param에는 K를 노출하는 .kind 연관 타입과 D를 노출하는 .default 연관 타입도 있습니다.

제네릭 호출 가능 객체

  • GenericCallable[Vs, lambda <vs>: Ty]: 제네릭 호출 가능 객체입니다. Vs는 바인딩되지 않은 타입 변수들의 튜플 타입이며, Ty는 <vs>의 바인딩된 변수를 통해 Vs의 변수에 접근할 수 있는 Callable, staticmethod 또는 classmethod여야 합니다.

현재로서는 GenericCallable을 Member의 타입 인자로만 사용하도록 제한하며, 지역 변수, 매개변수 타입, 반환 타입에 사용하거나 다른 타입 안에 중첩하는 등의 사용은 허용하지 않습니다. 근거는 아래에서 설명합니다.

오버로드된 함수 타입

  • Overloaded[*Callables] - 기반 타입들을 순서대로 담는 오버로드된 함수 타입입니다.

오류 발생

  • RaiseError[S: Literal[str], *Ts]: 실제 타입을 결정하기 위해 이 타입을 평가해야 하는 경우, 주어진 메시지로 타입 오류를 발생시킵니다.

    추가 타입 인자가 있다면 모두 메시지에 포함해야 합니다.

클래스 갱신

  • UpdateClass[*Ps: Member]: 기존 명목(nominal) 클래스를 새 멤버로 갱신하는 특수 형식입니다(기존 멤버를 재정의하거나, 타입을 Never로 만들어 제거할 수도 있습니다).

    이 형식은 타입 데코레이터의 반환 타입이나 __init_subclass__의 반환 타입으로만 사용할 수 있습니다.

    클래스가 선언될 때, 하나 이상의 조상 클래스에 UpdateClass 반환 타입을 가진 __init_subclass__가 있으면 이들은 MRO의 역순으로 적용됩니다. cls 매개변수가 type[T]로 매개변수화되어 있으면 T 자리에 해당 클래스 타입을 대입해야 합니다.

Union에 대한 리프팅

많은 내장 연산은 Union에 대해 “리프팅(lifting)”됩니다.

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

Slice[tuple[A, B, C], Literal[0] | Literal[1], Literal[2] | Literal[3]] = (
    tuple[A, B]
    | tuple[A, B, C]
    | tuple[B]
    | tuple[B, C]
)

연산이 유니온 타입에 대해 리프팅되면, 각 인자 위치의 유니온 요소들의 곱집합을 구하고, 곱집합의 각 튜플에 대해 연산자를 평가한 다음, 모든 결과를 하나의 유니온으로 합칩니다. Python으로 나타내면 이 로직은 다음과 같습니다.:

args_union_els = [get_union_elems(arg) for arg in args]
results = [
    eval_operator(*xs)
    for xs in itertools.product(*args_union_els)
]
if results:
    return Union[*results]
else:
    return Never

런타임 평가 지원

중요한 목표 중 하나는 이렇게 계산된 타입을 런타임에 평가할 수 있도록 지원하는 것입니다. 공식 평가기를 표준 라이브러리에 추가하는 것은 제안하지 않습니다. 대신 서드파티 평가기 라이브러리를 공개할 계획입니다.

타입 시스템 확장의 대부분은 “비활성” 타입 연산자 적용이지만, 이 문법에는 리스트 순회, 조건문, 속성 접근도 포함되며, 이들은 클래스, 별칭 또는 함수의 __annotate__ 메서드가 호출될 때 자동으로 평가됩니다.

이러한 경우에 평가기 라이브러리가 타입 평가를 시작할 수 있도록 typing에 새로운 훅을 추가합니다:

  • special_form_evaluator: 호출 가능 객체를 담는 ContextVar입니다. 이 호출 가능 객체는 불리언 연산자에 대해 __bool__이 호출되거나 typing.Iter에 대해 __iter__가 호출될 때 typing._GenericAlias 인자와 함께 호출됩니다. 그 반환값에는 반환되기 전에 bool 또는 iter가 호출됩니다.

    None으로 설정된 경우(기본값) 불리언 연산자는 False를 반환하고, Iter는 iter(())로 평가됩니다.

어노테이션을 가져오기 위한 Format.AST 모드를 추가하는 방안이 논의된 바 있습니다(이 PEP 초안을 참조하십시오). 이 모드를 사용하면 전혀 평가되지 않은 어노테이션도 여전히 쉽게 가져올 수 있으므로 이 제안과 매우 잘 어울립니다.

예제 / 튜토리얼

여기서는 튜토리얼 방식으로 동기 섹션의 예제에 나온 목표를 달성하는 방법을 살펴보고, 사용하는 기능은 사용하는 시점에 함께 설명합니다.

Prisma 스타일 ORM

먼저, 위에서 본 어노테이션을 지원하기 위해 제네릭 타입을 가진 더미 클래스 모음을 준비합니다.

class Pointer[T]:
    pass

class Property[T](Pointer[T]):
    pass

class Link[T](Pointer[T]):
    pass

class SingleLink[T](Link[T]):
    pass

class MultiLink[T](Link[T]):
    pass

select 메서드에서 새로운 요소가 보이기 시작합니다.

**kwargs: Unpack[K]는 이 제안의 일부이며, 키워드 인자로부터 TypedDict를 추론할 수 있게 합니다.

Attrs[K]는 K의 모든 타입 어노테이션된 속성에 대응하는 Member 타입을 추출하고, Member 인자로 NewProtocol을 호출하면 새로운 구조적 타입이 생성됩니다.

c.name은 변수 c에 바인딩된 Member의 이름을 리터럴 타입으로 가져옵니다. 이 모든 메커니즘은 리터럴 타입에 매우 크게 의존합니다. GetMemberType은 클래스에서 속성의 타입을 가져옵니다.

def select[ModelT, K: typing.BaseTypedDict](
    typ: type[ModelT],
    /,
    **kwargs: Unpack[K],
) -> list[
    typing.NewProtocol[
        *[
            typing.Member[
                c.name,
                ConvertField[typing.GetMemberType[ModelT, c.name]],
            ]
            for c in typing.Iter[typing.Attrs[K]]
        ]
    ]
]:
    raise NotImplementedError

ConvertField는 첫 번째 타입 헬퍼이며, (제한적인) 서브타입 비슷한 검사를 바탕으로 두 타입 중 하나를 선택하는 조건부 타입 별칭입니다.

ConvertField에서는 Property나 Link 어노테이션을 제거하고 그 기반 타입을 만들어 내려고 하며, 링크의 경우에는 프로퍼티만 포함하는 새 대상 타입을 만들고 MultiLink를 리스트로 감싸려고 합니다.

type ConvertField[T] = (
    AdjustLink[PropsOnly[PointerArg[T]], T]
    if typing.IsAssignable[T, Link]
    else PointerArg[T]
)

PointerArg는 Pointer 또는 그 서브클래스에 대한 타입 인자를 가져옵니다.

GetArg[T, Base, I]는 핵심 프리미티브 중 하나입니다. T가 Base를 상속하는 경우, 타입 T에서 Base에 대한 인덱스 I의 타입 인자를 가져옵니다.

(이에 관한 미묘한 부분은 나중에 논의합니다. 이 경우에는 단순히 Pointer에 대한 인자를 가져옵니다.)

type PointerArg[T] = typing.GetArg[T, Pointer, Literal[0]]

AdjustLink는 이미 논의한 기능을 사용하여 MultiLink를 list로 감쌉니다.

type AdjustLink[Tgt, LinkTy] = (
    list[Tgt] if typing.IsAssignable[LinkTy, MultiLink] else Tgt
)

그리고 마지막 헬퍼인 PropsOnly[T]는 T의 모든 Property 속성을 포함하는 새 타입을 생성합니다.

type PropsOnly[T] = typing.NewProtocol[
    *[
        typing.Member[p.name, PointerArg[p.type]]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[p.type, Property]
    ]
]

전체 테스트는 테스트 스위트에 있습니다.

FastAPI CRUD 모델 자동 도출

테스트 스위트에 더 완전한 예제가 있지만, 여기서는 Create만 구현하는 한 가지 방법을 보여 드립니다.:

# Extract the default type from an Init field.
# If it is a Field, then we try pulling out the "default" field,
# otherwise we return the type itself.
type GetDefault[Init] = (
    GetFieldItem[Init, Literal["default"]]
    if typing.IsAssignable[Init, Field]
    else Init
)

# Create takes everything but the primary key and preserves defaults
type Create[T] = typing.NewProtocol[
    *[
        typing.Member[
            p.name,
            p.type,
            p.quals,
            GetDefault[p.init],
        ]
        for p in typing.Iter[typing.Attrs[T]]
        if not typing.IsAssignable[
            Literal[True],
            GetFieldItem[p.init, Literal["primary_key"]],
        ]
    ]
]

Create 타입 별칭은 원래 타입의 속성을 순회하여 (NewProtocol을 통해) 새 타입을 생성합니다. 이 별칭은 이름, 타입, 한정자, 그리고 초기화 식의 리터럴 타입에 접근할 수 있습니다(일부는 여기서 사용된 매우 흔한 = Field(...) 형태의 패턴을 처리하기 위한 새로운 기능을 통해 이루어집니다).

여기서는 Field에 primary_key=True가 있는 속성을 걸러 내고, 기본 인자(필드에 대한 default 인자에서 오거나 초기화자로 직접 지정될 수 있습니다)도 추출합니다.

dataclasses 스타일 메서드 생성

InitFnType은 모든 속성을 순회하여 새 __init__ 함수에 대한 Member를 생성합니다.

여기서 GetDefault는 위의 FastAPI 유사 예제에서 가져온 것입니다.

# Generate the Member field for __init__ for a class
type InitFnType[T] = typing.Member[
    Literal["__init__"],
    Callable[
        typing.Params[
            typing.Param[Literal["self"], T],
            *[
                typing.Param[
                    p.name,
                    p.type,
                    # All arguments are keyword-only
                    Literal[ParamKind.KEYWORD_ONLY],
                    # GetDefault is Never when there's no default, so use it
                    # directly as D.
                    GetDefault[p.init],
                ]
                for p in typing.Iter[typing.Attrs[T]]
            ],
        ],
        None,
    ],
    Literal["ClassVar"],
]
type AddInit[T] = typing.NewProtocol[
    InitFnType[T],
    *[x for x in typing.Iter[typing.Members[T]]],
]

그런 다음 UpdateClass를 사용하여 클래스에 새 __init__ 메서드를 추가하는 클래스 데코레이터(@dataclass처럼)를 만들 수 있습니다.

def dataclass_ish[T](
    cls: type[T],
) -> typing.UpdateClass[
    # Add the computed __init__ function
    InitFnType[T],
]:
    raise NotImplementedError

또는 그렇게 하는 베이스 클래스(Pydantic처럼)를 만들 수도 있습니다.

class Model:
    def __init_subclass__[T](
        cls: type[T],
    ) -> typing.UpdateClass[
        # Add the computed __init__ function
        InitFnType[T],
    ]:
        pass

zip 유사 함수

타입 순회와 GetArg를 사용하면 zip에 올바른 타입을 부여할 수 있습니다.

type ElemOf[T] = typing.GetArg[T, Iterable, Literal[0]]

def zip[*Ts](
    *args: *Ts, strict: bool = False
) -> Iterator[tuple[*[ElemOf[t] for t in typing.Iter[tuple[*Ts]]]]]:
    return builtins.zip(*args, strict=strict)  # type: ignore[call-overload]

Slice 연산자와 타입 별칭 재귀를 사용하면, 서로 다른 타입의 튜플들을 함께 zip하는 경우에도 더 정확한 타입을 부여할 수 있습니다.

예를 들어, tuple[int, str]과 tuple[str, bool]을 zip하면 tuple[tuple[int, float], tuple[str, bool]]이 만들어져야 합니다.

def zip_pairs[*Ts, *Us](
    a: tuple[*Ts], b: tuple[*Us]
) -> Zip[tuple[*Ts], tuple[*Us]]:
    return cast(
        Zip[tuple[*Ts], tuple[*Us]],
        tuple(zip(a, b, strict=True)),
    )

type DropLast[T] = typing.Slice[T, Literal[0], Literal[-1]]
type Last[T] = typing.GetArg[T, tuple, Literal[-1]]

# Matching on Never here is intentional; it prevents infinite
# recursions when T is not a tuple.
type Empty[T] = typing.IsAssignable[typing.Length[T], Literal[0]]

Zip은 입력 튜플 중 하나 또는 둘 다가 빌 때까지 입력 튜플을 재귀적으로 따라 내려갑니다. 길이가 일치하지 않으면(하나만 비어 있기 때문에) 오류를 발생시킵니다.

type Zip[T, S] = (
    tuple[()]
    if typing.Bool[Empty[T]] and typing.Bool[Empty[S]]
    else typing.RaiseError[Literal["Zip length mismatch"], T, S]
    if typing.Bool[Empty[T]] or typing.Bool[Empty[S]]
    else tuple[*Zip[DropLast[T], DropLast[S]], tuple[Last[T], Last[S]]]
)

TypeScript 스타일 “유틸리티 타입”

TypeScript는 흔한 타입 연산을 수행하기 위한 여러 유틸리티 타입을 정의합니다.

그중 일부의 구현을 제시합니다.:

# Pick<T, Keys>
# Constructs a type by picking the set of properties Keys from T.
type Pick[T, Keys] = typing.NewProtocol[
    *[
        p
        for p in typing.Iter[typing.Members[T]]
        if typing.IsAssignable[p.name, Keys]
    ]
]

# Omit<T, Keys>
# Constructs a type by picking all properties from T and then removing Keys.
# Note that unlike in TS, our Omit does not depend on Exclude.
type Omit[T, Keys] = typing.NewProtocol[
    *[
        p
        for p in typing.Iter[typing.Members[T]]
        if not typing.IsAssignable[p.name, Keys]
    ]
]

# KeyOf[T]
# Constructs a union of the names of every member of T.
type KeyOf[T] = Union[*[p.name for p in typing.Iter[typing.Members[T]]]]

# Exclude<T, U>
# Constructs a type by excluding from T all union members assignable to U.
type Exclude[T, U] = Union[
    *[
        x
        for x in typing.Iter[typing.FromUnion[T]]
        if not typing.IsAssignable[x, U]
    ]
]

# Extract<T, U>
# Constructs a type by extracting from T all union members assignable to U.
type Extract[T, U] = Union[
    *[
        x
        for x in typing.Iter[typing.FromUnion[T]]
        # Just the inverse of Exclude, really
        if typing.IsAssignable[x, U]
    ]
]

# Partial<T>
# Constructs a type with all properties of T set to optional (T | None).
type Partial[T] = typing.NewProtocol[
    *[
        typing.Member[p.name, p.type | None, p.quals]
        for p in typing.Iter[typing.Attrs[T]]
    ]
]

# PartialTD<T>
# Like Partial, but for TypedDicts: wraps all fields in NotRequired
# rather than making them T | None.
type PartialTD[T] = typing.NewTypedDict[
    *[
        typing.Member[p.name, p.type, p.quals | Literal["NotRequired"]]
        for p in typing.Iter[typing.Attrs[T]]
    ]
]

근거

확장 호출 가능 객체

타입 수준 계산으로 호출 가능 객체를 검사하고 생성하려면 확장 호출 가능 객체 지원이 필요합니다. mypy는 확장 호출 가능 객체를 지원하지만, 이는 콜백 프로토콜로 대체되어 폐기 예정(deprecated) 상태입니다.

안타깝게도 콜백 프로토콜은 타입 수준 계산에 잘 맞지 않습니다. (아마 동작하게 만들 수는 있겠지만, 그러려면 메서드를 생성하고 내부 검사하기 위한 별도의 기능이 필요하며, 그것이 더 간단하지도 않을 것입니다.)

완전히 새로운 확장 호출 가능 객체 문법을 제안하는 이유는 다음과 같습니다.
  1. mypy_extensions 함수들은 아무 동작도 하지 않는(no-op) 함수이지만, 우리에게는 실제 런타임 객체가 필요합니다.
  2. 이 함수들은 대괄호가 아닌 소괄호를 사용하는데, 이는 이 제안의 철학에 정면으로 어긋납니다.
  3. 멤버를 검사하기 위해 우리가 하려는 방식과 더 잘 맞는 API를 만들 수 있습니다(다만 새로운 것을 도입하는 것이 도저히 받아들여지지 않는다면, mypy_extensions 버전을 거의 그대로 따르는 확장 호출 가능 객체를 도입할 수도 있습니다).

제네릭 호출 가능 객체

다음과 같은 시그니처를 가진 메서드를 생각해 봅시다.:

def process[T](self, x: T) -> T if IsAssignable[T, list] else list[T]:
    ...

이 메서드의 타입은 제네릭이며, 제네릭은 클래스가 아니라 메서드에 바인딩됩니다. 프로그래머가 NewProtocol에 대해 작성할 법한 형태로 이러한 제네릭 함수를 표현할 방법이 필요합니다.

다소 매력적이지만 동작하지 않는 한 가지 방법은 바인딩되지 않은 타입 변수를 사용하고 이를 일반화되도록 하는 것입니다.:

type Foo = NewProtocol[
    Member[
        Literal["process"],
        Callable[[T], T if IsAssignable[T, list] else list[T]]
    ]
]

문제는 이 방식이 런타임 평가 지원과 근본적으로 호환되지 않는다는 점입니다. 별칭 Foo를 평가하려면 IsAssignable도 평가해야 하므로, 적어도 조건문의 한쪽을 잃게 되기 때문입니다. 제네릭 함수를 가진 클래스에 대해 Members를 평가할 때도 비슷한 문제가 발생합니다. 본문을 람다로 감싸면 두 경우 모두 평가를 지연시킬 수 있습니다. (Members에서 평가를 지연하는 경우는 명시적인 제네릭 어노테이션을 가진 함수에 대해 꽤 잘 동작합니다. 구식 제네릭의 경우에는 아마 평가를 시도하다가 변수를 만나면 오류를 발생시켜야 할 것입니다.)

실제 문법으로는 다음과 같습니다.:

type Foo = NewProtocol[
    Member[
        Literal["process"],
        GenericCallable[
            tuple[T],
            lambda T: Callable[[T], T if IsAssignable[T, list] else list[T]],
        ],
    ]
]

GenericCallable의 사용을 Member의 타입 인자로 제한할 것을 제안하는 이유는, 비서술적(impredicative) 다형성(타입 변수를 다른 제네릭 타입으로 인스턴스화할 수 있는 경우)과 랭크-N 타입(함수 타입 내부 깊숙이 중첩된 위치에서 제네릭이 바인딩될 수 있는 경우)이 타입 추론과 결합되면 골칫거리가 되기 때문입니다 [1]. 지원하면 좋겠지만, 지금 그 골칫거리를 떠안고 싶지는 않습니다.

바인딩되지 않은 타입 변수 튜플은 경계(bound), 기본값, TypeVarTuple 여부를 지정할 수 있도록 하기 위한 것이지만, 어쩌면 새로운 접근 방식을 고안해야 할 수도 있습니다.

하위 호환성

가장 엄밀한 의미에서 이 PEP는 새로운 기능만을 제안하므로 하위 호환성 문제가 없어야 합니다.

하지만 좀 더 느슨하게 말하자면, 타입 어노테이션에서 if와 for를 사용하면 타입 어노테이션을 추출하려는 도구에 문제가 생길 수 있습니다.

어노테이션을 완전히 평가하려는 도구는 평가기를 직접 구현하거나 이를 위한 라이브러리를 사용해야 합니다(PEP 작성자들은 그러한 라이브러리를 만들 계획입니다).

런타임에 어노테이션을 인트로스펙션하는 데 특별히 의존하면서(Python 파일을 파싱하는 도구는 당연히 영향을 받지 않습니다) 어노테이션을 평가되지 않은 상태로 추출하여 어떤 방식으로 처리하려는 도구는 더 큰 어려움을 겪을 수 있습니다. 현재는 from __future__ import annotations가 지정되어 있다면 이것이 가능합니다. 문자열 어노테이션을 ast.parse로 파싱한 다음 임의의 방식으로 처리할 수 있기 때문입니다.

그렇지 않은 경우, 현재 상황에서는 일이 더 까다로워집니다. 현재로서는 어노테이션을 실행하지 않고는 __annotate__ 함수에서 유용한 정보를 얻을 방법이 없으며, 문자열을 만들기 위한 그 요령들은 반복문과 조건문에는 통하지 않기 때문입니다.

이는 다음 중 하나를 통해 완화할 수 있습니다.
  1. PEP 649의 “Just store the strings” 옵션으로, 이를 통해 항상 평가되지 않은 문자열을 추출할 수 있습니다.
  2. 어노테이션을 가져오기 위한 Format.AST 모드를 추가합니다(이 PEP 초안 참조).

두 방법 중 어느 쪽도 택하지 않는다면, 평가되지 않은 타입 조작 표현식을 처리하려는 도구는 아마 소스 코드를 다시 파싱하여 거기에서 어노테이션을 추출해야 할 것입니다. 어차피 대부분의 도구가 그렇게 하고 있을 것으로 예상합니다.

보안 영향

예상되는 영향은 없습니다.

교육 방법

TypeScript도 비슷한 복잡도를 지닌 동등한 기능을 가르치고 있으므로, TypeScript의 교육 방식에서 많은 영감을 얻을 수 있다고 생각합니다. TypeScript처럼 예제 중심의 고수준 문서가 필요할 것입니다.

또한 새 문법과 API를 사용할 것으로 예상되는 대상은, 자신들이 도입하는 고급 패턴과 API를 지원하기 위해 타입 조작을 구현할 프레임워크 및 라이브러리 유지 관리자라는 점에 유의해야 합니다.

참조 구현

런타임 평가기 데모가 있으며, 현재 이 PEP 초안도 그곳에 있습니다.

mypy에서 개념 증명 구현이 진행 중입니다.

이 구현은 이 문서의 모든 예제를 타입 검사할 수 있습니다.

대안 문법 아이디어

일명 ‘실제로는 해야 할지도 모르는 “거부된” 아이디어?’

이에 대한 피드백을 매우 환영합니다!

typed dict와 프로토콜 생성을 위한 딕셔너리 컴프리헨션 기반 문법

이는 어떤 면에서 인라인 typed dictionary에 대한 PEP 764 (아직 초안) 제안의 확장입니다.

위 제안과 결합하여 NewProtocol에 사용하면 (쿼리 빌더 예제의 내용을 사용하여) 대략 다음과 같은 모습이 될 것입니다:

type PropsOnly[T] = typing.NewProtocol[
    {
        p.name: PointerArg[p.type]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[p.type, Property]
    }
]

그러면 한정자 및/또는 초기화 값 타입을 지정하고 싶은 경우를 위해 Member를 지정하는 것도 허용하고 싶을 것입니다(단, Name이 마지막에 오고 기본값을 갖도록 순서를 바꿉니다).

한정자를 타입 안에 작성하도록 허용하는 방안도 생각할 수 있지만, 이는 타입 표현식이 아니라 어노테이션 표현식이므로 다소 어색하며, 조건부 타입의 분기에 어노테이션 표현식을 두는 것은 아마 허용되지 않을 것 아니겠습니까?

이 제안의 주된 단점은 복잡성뿐입니다. 또 다른 종류의 기묘한 타입 형식을 도입해야 합니다.

또한 TypedDict와 새 프로토콜 사이의 정확한 상호 작용도 정해야 합니다. 딕셔너리 문법이 항상 typed dict를 생성하고 NewProtocol이 이를 프로토콜로 변환하는 것입니까, 아니면 NewProtocol[<dict type expr>]가 특수 형식이 되는 것입니까? ClassVar와 Final도 허용하려 하겠습니까?

구조 분해?

또 다른 잠재적 “단점”(실제로는 장점일 수도 있습니다!)은 Attrs와 Members를 items() 스타일 이터레이터로 순회할 수 있기를 바라게 될 수 있음을 시사한다는 점이며, 이는 더 복잡한 질문을 제기합니다.

먼저, 문법은 대략 다음과 같을 것입니다:

type PropsOnly[T] = typing.NewProtocol[
    {
        k: PointerArg[ty]
        for k, ty in typing.IterItems[typing.Attrs[T]]
        if typing.IsAssignable[ty, Property]
    }
]

꽤 보기 좋지만, 이름과 타입에만 접근할 수 있을 뿐 한정자나 초기화 값에는 접근할 수 없습니다.

이를 처리할 수 있는 방안은 다음과 같습니다:

  • 괜찮습니다. 프로그래머는 일반적인 경우에는 이 .items() 스타일 이터레이터를 사용하고, 필요할 때는 완전한 Member 객체를 다루면 됩니다.
  • 한정자/초기화 값을 key에 넣을 수 있지 않겠습니까? 그러면 실제로 이름을 사용하려면 key.name 같은 것을 해야 합니다.

(또한 이런 방식으로 무엇을 순회할 수 있는지에 관한 규칙이 정확히 무엇인지도 정해야 합니다.)

괄호를 사용해 타입 연산자 호출하기

사람들이 Bracket City에서 힘든 시간을 보내고 있다면, 내장 타입 연산자가 대괄호 대신 소괄호를 사용하도록 하는 방안도 고려할 수 있습니다.

[]와 ()를 섞어 쓰면 일관성 문제가 생기고, 사용자는 어떤 API가 대괄호를 쓰고 어떤 API가 소괄호를 쓰는지 기억해야만 합니다. 현재 Python 타이핑이 대괄호 사용을 중심으로 이루어져 있다는 점을 고려할 때, 그 방향을 계속 유지하는 것이 더 나은 개발자 경험으로 이어질 것이라고 강하게 생각합니다.

예를 들어, ()와 []를 섞으면 다음과 같은 모습이 될 수 있습니다:

type PropsOnly[T] = typing.NewProtocol(
    {
        p.name: PointerArg[p.type]
        for p in typing.Iter(typing.Attrs(T))
        if typing.IsAssignable(p.type, Property)
    }
)

(사용자 정의 타입 별칭 PointerArg는 사실상 보조 연산자임에도 여전히 대괄호로 호출해야 합니다.)

점 표기법으로 접근할 수 있는 연관 타입을 위한 일반적인 메커니즘 두기

주 제안은 현재 Member와 Param이 .name과 .type에 대한 연관 타입을 정확히 어떻게 갖게 될지에 대해 언급하지 않습니다.

해당 특정 타입들에 대해서만 동작하게 만들 수도 있고, 다음과 비슷한 형태의 일반적인 메커니즘을 도입할 수도 있습니다:

@typing.has_associated_types
class Member[
    N: str,
    T,
    Q: MemberQuals = typing.Never,
    I = typing.Never,
    D = typing.Never
]:
    type name = N
    type tp = T
    type quals = Q
    type init = I
    type definer = D

연관 타입에 대한 점 표기법이 런타임에 동작하게 하려면 데코레이터(또는 베이스 클래스)가 필요합니다. 클래스가 만들어 내는 typing._GenericAlias에서 __getattr__의 동작을 사용자 정의하여 Member에 전달된 타입 매개변수와 별칭을 모두 포착하도록 해야 하기 때문입니다.

(다만 _GenericAlias 자체의 동작을 변경하여 그럴 필요를 없앨 수도 있을 것입니다.)

기각된 아이디어

런타임 평가에 대한 고려를 모두 포기하기

이렇게 하면 구문 형태를 실험할 수 있는 유연성이 더 커지고, 언패킹된 컴프리헨션 타입에서 typing.Iter를 요구하는 것이나 조건부 타입에 나타날 수 있는 <type-bool> 표현식을 제한된 집합으로 두는 것과 같은 몇 가지 보기 흉한 부분을 없앨 수 있습니다.

하지만 좋든 나쁘든 타입 어노테이션의 런타임 사용은 널리 퍼져 있습니다. 예를 들어 pydantic이 이에 의존하며, 이 제안의 동기가 된 예시 중 하나(FastAPI CRUD 모델 자동 도출)도 이에 의존합니다.

서브타입 검사에서 TypeScript 스타일 패턴 매칭 지원하기

TypeScript에서 조건부 타입은 다음과 같이 구성됩니다:

SomeType extends OtherType ? TrueType : FalseType

게다가 검사의 우변에서는 infer 키워드를 사용하여 패턴 매칭을 기반으로 타입 변수를 바인딩할 수 있습니다. 배열의 요소 타입을 추출하는 다음 예시와 같습니다:

type ArrayArg<T> = T extends [infer El] ? El : never;

이는 매우 우아한 메커니즘이며, 특히 typing.GetArg와 그 미묘한 Base 매개변수가 필요 없게 만든다는 점에서 그렇습니다.

안타깝게도 특히 미묘한 바인딩 구조 때문에, 이를 Python의 기존 문법에 만족스러운 방식으로 끼워 맞추기는 매우 어려워 보입니다.

아마도 가장 그럴듯한 변형은 다음과 같은 형태일 것입니다:

type ArrayArg[T] = El if IsAssignable[T, list[Infer[El]]] else Never

그런 다음 이를 런타임에 평가하려면, 바인딩되지 않은 Infer 인자를 잡아내는 사용자 정의 globals 환경을 사용하는 등의 복잡한 작업을 해야 합니다.

또한 큰 구문 변경(삼항 연산자 대신 타입 연산자 사용) 없이는 조건부를 유니온 위로 들어 올리는 TypeScript의 동작과 맞출 수 없습니다.

IsAssignable을 “할당 가능” 검사보다 약한 것으로 대체하기

완전한 Python 타이핑 할당 가능성 검사는 런타임에 완전히 구현할 수 없습니다(특히, 표준 라이브러리에 대한 typeshed 타입을 모두 사용할 수 있게 하더라도, 클래스 속성이 추론될 수 있고 런타임에 눈에 보이는 존재가 없을 수 있기 때문에 프로토콜에 대한 검사는 불가능한 경우가 많습니다).

제안된 대로라면 런타임 평가기는 “최선의 노력”을 기울이는 방식이어야 하며, 그 노력의 윤곽이 잘 문서화되어 있는 것이 이상적입니다.

대안적인 접근 방식은 더 약한 술어를 핵심 기본 요소로 두는 것입니다.

한 가지 가능성은 “하위 유사성” 검사입니다. IsAssignableSimilar는 본질적으로 타입 매개변수를 보지 않고 타입의 머리 부분에 대해 단순한 검사만 수행합니다. 이는 프로토콜에서는 동작하지 않습니다. 그래도 여전히 유니온 위로 들어 올려지며 리터럴도 검사합니다.

서브타이핑과 비슷하지만 같지는 않은 새로운 개념을 도입하는 것은 아마도 좋은 생각이 아니라고 판단했습니다. 그런 개념은 IsAssignableSimilar 같은 길고 이상한 이름을 갖거나 IsAssignable 같은 오해를 부르는 짧은 이름을 가져야 하기 때문입니다.

Member 구성 요소에 접근할 때 점 표기법 사용하지 않기

이 PEP 초안의 이전 버전에서는 Member와 Param 구성 요소에 m.name 같은 표현을 쓰는 기능을 빼고, 대신 typing.GetName 같은 도우미 연산자에 의존했습니다(이 연산자는 내부적으로 typing.GetArg나 typing.GetMemberType을 사용해 구현할 수 있습니다).

이 방식의 잠재적인 장점은 타입 언어에 추가되는 새로운 구성 요소의 수를 줄이고, 연관 타입을 위한 새로운 일반 메커니즘을 도입하거나 Member를 위한 특수 사례를 두어야 하는 상황을 피할 수 있다는 것입니다.

(쿼리 빌더 예제의) PropsOnly는 다음과 같은 모습이 됩니다:

type PropsOnly[T] = typing.NewProtocol[
    *[
        typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[typing.GetType[p], Property]
    ]
]

조건과 반복에 타입 연산자 사용하기

다음과 같이 작성하는 대신:
  • tt if tb else tf
  • *[tres for T in Iter[ttuple]]
다음과 같은 타입 연산자 형식을 사용할 수 있습니다:
  • Cond[tb, tt, tf]
  • UnpackMap[ttuple, lambda T: tres]
  • 또는 T가 선언된 TypeVar여야 하는 UnpackMap[ttuple, T, tres]

불리언 연산도 마찬가지로 연산자(Not, And 등)가 됩니다.

이 방식의 장점은 (점 표기법도 없앤다고 가정하면) 타입 어노테이션을 구성할 때 사소하지 않은 계산을 할 필요가 전혀 없으므로, 이를 평가하기 위한 런타임 훅이 필요하지 않다는 것입니다.

또한 원시 타입 어노테이션을 훨씬 쉽게 추출할 수 있게 됩니다. (람다 형식은 여전히 다소 까다로울 것입니다. 람다가 아닌 형식은 추출하기가 아주 쉽지만, TypeVar 선언을 요구하는 것은 최근 변경 사항의 흐름에 어긋납니다.)

특별한 <type-bool> 타입 부류라는 개념이 전혀 필요 없다는 것도 또 다른 장점입니다.

단점은 문법이 훨씬 나빠 보인다는 것입니다. 매핑하면서 필터링까지 지원하면 더욱 나빠질 것입니다(필터를 위한 추가 인자를 두는 방식이 될 수도 있습니다).

필요하다면 다른 선택지도 검토할 수 있습니다.

일반 Python 함수로 타입 조작 수행하기

한 가지 제안은 타입 수준 조작을 위해 새로운 타입 언어 조각을 정의하는 대신, 일종의 “미니 mypy 플러그인” 역할을 하는 Python 함수(의 일부 부분 집합)를 호출할 수 있도록 지원하자는 것이었습니다.

(저희가 보기에) 이 방식의 주된 장점은 더 익숙한 실행 모델을 활용할 수 있다는 것입니다.

제안된 장점 중 하나는 이것이 제안을 단순화한다는 것이지만, 저희는 이 아이디어가 약속하는 단순화가 대부분 신기루에 불과하며, 타입을 조작하기 위해 Python 함수를 호출하는 것은 오히려 상당히 더 복잡할 것이라고 생각합니다.

이를 위해서는 타입 검사기 내부에서 실행할 수 있도록, 잘 정의되고 안전하게 실행할 수 있는 언어(및 표준 라이브러리)의 부분 집합을 정의해야 합니다. 이와 같은 부분 집합은 다른 시스템에서도 정의된 바 있지만(Bazel의 설정 언어인 Starlark 참고), 여전히 다뤄야 할 범위가 넓고, 프로그래머는 그 경계를 항상 염두에 두어야 합니다.

또한 “미니 플러그인” 함수에서 타입이 어떻게 표현되는지에 대한 명확한 명세가 필요하며, 다양한 조작을 수행하기 위한 함수/메서드도 정의해야 합니다. 그러한 함수들은 이 PEP가 현재 제안하는 내용과 상당히 많이 겹칠 것입니다.

런타임 사용이 필요하다면, 타입 표현을 현재 typing이 동작하는 방식과 호환되도록 만들거나, 서로 다른 두 가지 런타임 타입 표현을 두어야 합니다.

이것이 문법을 개선할지는 더 논란의 여지가 있습니다. 저희는 앞서 논의한(그러나 아직 주 제안에 통합되지 않은) 문법 정리 아이디어 중 일부를 채택하는 편이 더 적은 비용으로 문법 상황을 개선할 것이라고 생각합니다.

타입 수준 연산을 더 “엄격하게 타입 지정”하기

이 제안은 TypeScript보다 덜 “엄격하게 타입 지정”되어 있습니다(엄격하게 카인드 지정되어 있다고 하는 편이 더 정확할 수도 있습니다).

TypeScript는 별칭 정의 위치에서 더 나은 타입 검사를 제공합니다. P[K]의 경우 K가 keyof P를 만족해야 합니다. extends 조건부 타입 연산자는 타입을 좁혀서 이를 뒷받침합니다.

TypeScript에서는 확장 시점에 실패하는 타입 별칭을 정의할 수 없지만, 이 시스템에서는 그렇게 하는 것이 가능합니다.

이것 역시 불가능하도록 만들 수도 있겠지만, 그러려면 상당히 많은 장치가 추가로 필요합니다.

  • KeyOf[T] - T의 리터럴 키
  • 타입 별칭을 정적으로 검사할 때 Member[T]는 tuple[Member[KeyOf[T], object, str, ..., ...], ...]와 같은 타입을 가진 것으로 취급될 수 있습니다.
  • GetMemberType[T, S: KeyOf[T]] - 인덱스가 키여야 한다는 바운드를 GetMember에 부여합니다… 하지만 이런 종류의 의존적 바운드는 현재 지원되지 않습니다. (TypeScript는 이를 지원합니다.)
  • 또한 문맥에 따른 타입 바운드 추론도 수행해야 합니다. 이는 미묘한 문제이지만, 이런 종류의 작업은 분명히 항(term) 수준에서 이미 이루어지고 있습니다.

저희는 이것이 복잡성을 감수할 만한 가치가 없으며, 더 낫다는 것조차 분명하지 않다고 생각합니다. TypeScript에서는 흔히 많은 조건문을 작성해야 하는데, 이때 대개 항상 참 브랜치를 타도록 의도됩니다. 보통 거짓 브랜치는 never를 반환하며, 이러한 경우는 추적하기가 꽤 어려울 수 있습니다.

향후 확장 가능성

Annotated 조작 지원

FastAPI 같은 라이브러리는 어노테이션을 많이 사용하며, 저희는 어노테이션을 사용해 타입 수준 계산의 의사 결정을 이끌 수 있기를 바랍니다.

현재 Annotated는 타입 검사기가 완전히 무시할 수도 있으므로, 이를 검사하고 조작하는 기능을 지원하는 것은 문제가 많을 수 있다는 점에 유의하십시오.

이를 위한 API의 한 가지 후보는 다음과 같습니다:

  • GetAnnotations[T] - Annotated일 수 있는 타입의 어노테이션을 리터럴로 가져옵니다. 예시:
    GetAnnotations[Annotated[int, 'xxx']] = Literal['xxx']
    GetAnnotations[Annotated[int, 'xxx', 5]] = Literal['xxx', 5]
    GetAnnotations[int] = Never
    
  • DropAnnotations[T] - Annotated 타입일 수 있는 타입의 어노테이션을 제거합니다. 예시:
    DropAnnotations[Annotated[int, 'xxx']] = int
    DropAnnotations[Annotated[int, 'xxx', 5]] = int
    DropAnnotations[int] = int
    

문자열 조작

TypeScript에는 문자열을 위한 “템플릿 리터럴” 타입이 있어 문자열 리터럴 타입을 연결할 수도 있고 분해할 수도 있습니다. 또한 대소문자 변환과 관련된 일련의 연산도 제공합니다.

연결을 지원하면 속성을 기반으로 새 메서드 이름을 생성하는 것과 같은 사용 사례가 가능해집니다: 모든 속성 foo에 대해 get_foo 메서드를 생성할 수 있습니다.

슬라이싱을 지원하면 더 심도 있는 문자열 순회가 가능해지고, 대소문자 변환을 지원하면 이름을 snake_case에서 CapitalizedWords로 변환하는 것과 같은 연산이 가능해집니다.

실제로 대소문자 함수는 이들과 여러 조건문을 사용해 구현할 수 있지만, 그렇게 해서는 안 됩니다(특히 모든 유니코드에서 동작하기를 원한다면 더욱 그렇습니다!).

또한 슬라이싱과 연결만 채택하는 것도 분명히 가능합니다.

  • Slice[S: Literal[str], Start: Literal[int | None], End: Literal[int | None]]: 문자열 타입의 슬라이싱도 지원합니다. (현재는 튜플이 지원됩니다.)
  • Concat[S1: Literal[str], S2: Literal[str]]: 두 문자열을 연결합니다
  • Uppercase[S: Literal[str]]: 문자열 리터럴을 대문자로 변환합니다
  • Lowercase[S: Literal[str]]: 문자열 리터럴을 소문자로 변환합니다
  • Capitalize[S: Literal[str]]: 문자열 리터럴의 첫 글자를 대문자로 변환합니다
  • Uncapitalize[S: Literal[str]]: 문자열 리터럴의 첫 글자를 소문자로 바꿉니다

이 절의 모든 연산자는 유니언 타입에 대해 리프팅됩니다.

NewProtocolWithBases

때로는 다음과 같은 명세를 갖는 NewProtocolWithBases 같은 것을 지원하면 유용할 것입니다:

  • NewProtocolWithBases[Bases: tuple[type], *Ms: Member]

어떤 타입이 주어진 베이스를 모두 확장하고 지정된 멤버를 가지고 있으면 이 프로토콜을 만족한다는 개념입니다.

이는 새로운 Pydantic 모델을 생성하는 것과 같은 작업을 하려는 상황에서 유용할 것입니다.

베이스를 갖는 프로토콜은 프로토콜이 될 수 있는 범위에 새로운 것을 더하는 일이어서 아직은 손대고 싶지 않고, 많은 사용 사례를 다른 방법으로 흉내 낼 수 있으므로, 현재로서는 이를 정식으로 제안하는 것을 보류합니다.

미해결 문제

  • **kwargs에 대한 타입 변수의 Unpack: 추가 인자에 대해 리터럴 타입 추론을 시도할지 여부를 바운드 역할을 하는 TypedDict에서 어떤 식으로든 설정할 수 있어야 합니까? readonly가 TypedDict의 매개변수로 추가되었다면 그것을 사용했겠지만, 추가되지 않았습니다.
  • Members: Members가 어노테이션이 없는 메서드까지 포함하여 모든 메서드를 반환해야 합니까? 속성과 어느 정도 일관성을 유지하려고 이를 제외했지만, 정적 평가기나 런타임 평가기 어느 쪽에서든 이를 포함하는 것은 기술적으로 어렵지 않을 것입니다.
  • 제네릭 호출 가능 객체: GenericCallable을 검사하거나 분해하는 메커니즘이 필요합니까? 어쩌면 변수 정보를 가져와서 구체적인 타입에 적용할 수도 있지 않겠습니까?
  • 클래스 갱신: UpdateClass는 타입 평가 순서에 대한 의존성을 도입합니다. 어떤 __init_subclass__에 대한 UpdateClass 반환 타입이 관련 없는 다른 클래스의 Members를 검사하고 그 클래스에도 __init_subclass__가 있다면, 결과는 평가 순서에 따라 달라질 수 있습니다. 이상적으로는 이런 경우를 거부해야 합니다. 다만 이는 실제로 잠재적인 런타임 평가 순서 의존성을 그대로 반영하는 것이기도 합니다.
  • RaiseError가 타입을 출력할 때 문자열 템플릿을 지원해야 합니까?
  • 제네릭 함수 때문에 (해결되지 않은 타입 변수에 적용되어) 타입 연산자를 평가할 수 없는 경우가 많을 것이며, 그런 경우에 타입 평가 규칙이 정확히 어떠해야 하는지는 다소 불분명합니다.

    현재 mypy의 개념 증명 구현에서는 평가가 막힌 타입에 대해 서브타입 검사를 완전히 불변적으로 수행합니다. 즉, 연산자가 일치하는지, 그리고 모든 피연산자가 양쪽 인자에서 불변적으로 일치하는지 검사합니다.

감사의 글

설계에 관해 많은 논의를 함께해 준 Jukka Lehtosalo에게 감사드립니다.

또한 이 제안에 상당한 영향을 준 언어를 만든 TypeScript 팀에게도 감사드립니다!

각주

  • Hans Boehm, “Partial polymorphic type inference is undecidable”: https://dl.acm.org/doi/10.1109/SFCS.1985.44
  • Frank Pfenning, “On the Undecidability of Partial Polymorphic Type Reconstruction”: https://www.cs.cmu.edu/~fp/papers/CMU-CS-92-105.pdf

    다만 이 제안의 설정에서는 함수의 제네릭 타입을 추론하려 하지 않으므로 이러한 문제 중 일부를 피할 수 있을지도 모릅니다. 반면에 이 설정에는 서브타이핑이 있습니다. (솔직히 말해 우리는 이미 이런 골치 아픈 문제 중 일부에 꽤 깊이 빠져 있습니다.)