[Final] PEP 484 - Type Hints

원문 링크: PEP 484 - Type Hints

상태: Final 유형: Standards Track 작성일: 29-Sep-2014

PEP 484 – 타입 힌트 (Type Hints) 한국어 번역 및 정리

개요

PEP 3107은 함수 어노테이션(function annotations)을 위한 문법을 도입했지만, 그 의미는 의도적으로 정의되지 않은 채로 남겨두었습니다. 이제 정적 타입 분석을 위한 서드파티(3rd party) 사용이 충분히 이루어져, 커뮤니티는 표준 라이브러리 내에서 표준 어휘와 기본 도구를 제공함으로써 이점을 얻을 수 있게 되었습니다.

이 PEP는 이러한 표준 정의와 도구를 제공하기 위한 임시(provisional) 모듈을 소개하며, 어노테이션을 사용할 수 없는 상황을 위한 일부 규칙도 함께 제시합니다.

이 PEP는 어노테이션의 다른 사용을 명시적으로 막지 않으며, 본 사양을 따르는 어노테이션이라 할지라도 특정 처리를 요구하거나 금지하지 않습니다. 단지 PEP 333이 웹 프레임워크에 그랬듯이, 더 나은 조정을 가능하게 할 뿐입니다.

예를 들어, 다음은 인자와 반환 타입이 어노테이션으로 선언된 간단한 함수입니다.

def greeting(name: str) -> str:
    return 'Hello ' + name

이러한 어노테이션은 런타임에 일반적인 __annotations__ 속성을 통해 사용할 수 있지만, 런타임에 타입 검사는 발생하지 않습니다. 대신, 이 제안은 사용자가 자발적으로 소스 코드에 대해 실행할 수 있는 별도의 오프라인 타입 체커(off-line type checker)의 존재를 가정합니다. 기본적으로 이러한 타입 체커는 매우 강력한 린터(linter) 역할을 합니다.

이 제안은 mypy로부터 강하게 영감을 받았습니다. 예를 들어, “정수 시퀀스” 타입은 Sequence[int]로 작성할 수 있습니다. 대괄호는 언어에 새로운 문법을 추가할 필요가 없음을 의미합니다. 여기서는 순수 Python 모듈 typing에서 임포트된 사용자 정의 타입 Sequence를 사용합니다. Sequence[int] 표기법은 메타클래스에서 __getitem__()을 구현하여 런타임에 작동합니다 (하지만 그 중요성은 주로 오프라인 타입 체커에 있습니다).

타입 시스템은 유니언(unions), 제네릭(generic types), 그리고 모든 타입과 일관성 있는(즉, 할당 가능하고 할당 받을 수 있는) Any라는 특수 타입을 지원합니다. 이 후자의 기능은 점진적 타이핑(gradual typing) 아이디어에서 가져왔습니다. 점진적 타이핑과 전체 타입 시스템은 PEP 483에 설명되어 있습니다.

배경 및 목표

PEP 3107은 함수 정의의 일부에 임의의 어노테이션을 추가하는 기능을 추가했습니다. 당시 어노테이션에 의미가 부여되지는 않았지만, 해당 PEP에서 첫 번째 가능한 사용 사례로 나열된 것처럼 항상 타입 힌트를 위한 사용이라는 암묵적인 목표가 있었습니다.

이 PEP는 타입 어노테이션을 위한 표준 문법을 제공하여, Python 코드를 더 쉬운 정적 분석 및 리팩토링, 잠재적인 런타임 타입 검사, 그리고 (일부 컨텍스트에서는) 타입 정보를 활용한 코드 생성을 가능하게 하는 것을 목표로 합니다.

이러한 목표 중 정적 분석이 가장 중요합니다. 여기에는 mypy와 같은 오프라인 타입 체커 지원뿐만 아니라, IDE가 코드 완성 및 리팩토링에 사용할 수 있는 표준 표기법을 제공하는 것이 포함됩니다.

비목표 (Non-goals)

제안된 typing 모듈은 런타임 타입 검사를 위한 일부 구성 요소를 포함하겠지만 (특히 get_type_hints() 함수), 특정 런타임 타입 검사 기능(예: 데코레이터나 메타클래스 사용)을 구현하기 위해서는 서드파티 패키지가 개발되어야 합니다. 성능 최적화를 위한 타입 힌트 사용은 독자의 몫으로 남겨둡니다.

또한, Python은 동적 타입 언어로 남을 것이며, 작성자들은 타입 힌트를 관례적으로라도 필수로 만들 의사가 전혀 없다는 점을 강조해야 합니다.

어노테이션의 의미

어노테이션이 없는 모든 함수는 타입 체커에 의해 가능한 가장 일반적인 타입으로 처리되거나 무시되어야 합니다. @no_type_check 데코레이터가 있는 함수는 어노테이션이 없는 것으로 처리되어야 합니다.

타입이 검사되는 함수는 모든 인자와 반환 타입에 대한 어노테이션을 갖는 것이 권장되지만 필수는 아닙니다. 타입이 검사되는 함수의 경우, 인자와 반환 타입에 대한 기본 어노테이션은 Any입니다. 인스턴스 및 클래스 메서드의 첫 번째 인자는 예외입니다. 어노테이션이 없으면, 인스턴스 메서드의 경우 포함하는 클래스의 타입, 클래스 메서드의 경우 포함하는 클래스 객체에 해당하는 타입 객체 타입으로 가정됩니다. 예를 들어, 클래스 A에서 인스턴스 메서드의 첫 번째 인자는 암묵적으로 A 타입을 가집니다. 클래스 메서드에서는 첫 번째 인자의 정확한 타입을 사용 가능한 타입 표기법으로 나타낼 수 없습니다.

(참고: __init__의 반환 타입은 -> None으로 어노테이션되어야 합니다. 이에 대한 이유는 미묘합니다. 만약 __init__-> None 반환 어노테이션을 가정한다면, 인자 없는 어노테이션 없는 __init__ 메서드도 타입 검사를 받아야 한다는 의미일까요? 이를 모호하게 두거나 예외에 대한 예외를 도입하는 대신, 우리는 __init__이 반환 어노테이션을 가져야 한다고 간단히 말합니다. 따라서 기본 동작은 다른 메서드와 동일합니다.)

타입 체커는 주어진 어노테이션과의 일관성을 위해 검사되는 함수의 본문을 검사할 것으로 예상됩니다. 어노테이션은 다른 검사되는 함수에 나타나는 호출의 정확성을 검사하는 데도 사용될 수 있습니다.

타입 체커는 필요한 만큼 많은 정보를 추론하려고 시도할 것으로 예상됩니다. 최소 요구 사항은 내장 데코레이터 @property, @staticmethod, @classmethod를 처리하는 것입니다.

타입 정의 문법 (Type Definition Syntax)

이 문법은 PEP 3107 스타일의 어노테이션을 활용하며, 아래 섹션에 설명된 여러 확장이 있습니다. 기본 형식에서 타입 힌팅은 함수 어노테이션 슬롯을 클래스로 채워서 사용됩니다.

def greeting(name: str) -> str:
    return 'Hello ' + name

이는 name 인자의 예상 타입이 str임을 나타냅니다. 유사하게, 예상 반환 타입은 str입니다.

타입이 특정 인자 타입의 서브타입인 표현식도 해당 인자에 대해 허용됩니다.

허용되는 타입 힌트 (Acceptable type hints)

타입 힌트는 내장 클래스(표준 라이브러리 또는 서드파티 확장 모듈에 정의된 클래스 포함), 추상 베이스 클래스(ABCs), types 모듈에서 사용 가능한 타입, 그리고 사용자 정의 클래스(표준 라이브러리 또는 서드파티 모듈에 정의된 클래스 포함)일 수 있습니다.

어노테이션이 일반적으로 타입 힌트를 위한 최상의 형식일지라도, 특별한 주석으로 표현하거나 별도로 배포된 스텁 파일에 표현하는 것이 더 적절한 경우가 있습니다.

어노테이션은 함수가 정의될 때 예외를 발생시키지 않고 평가되는 유효한 표현식이어야 합니다 (단, 선언되지 않은 이름 참조 (forward references)는 아래 참조).

어노테이션은 간단하게 유지되어야 하며, 그렇지 않으면 정적 분석 도구가 값을 해석하지 못할 수 있습니다. 예를 들어, 동적으로 계산된 타입은 이해하기 어려울 수 있습니다. (이는 의도적으로 다소 모호한 요구 사항이며, 논의에 따라 이 PEP의 향후 버전에 특정 포함 및 제외가 추가될 수 있습니다.)

위 사항 외에도, 아래에 정의된 다음 특수 구문이 사용될 수 있습니다: None, Any, Union, Tuple, Callable, typing에서 내보낸 모든 ABC 및 구체 클래스의 대체 (예: SequenceDict), 타입 변수, 그리고 타입 별칭.

이어지는 섹션에서 설명된 기능을 지원하기 위해 새로 도입된 모든 이름(예: AnyUnion)은 typing 모듈에서 사용할 수 있습니다.

None 사용 (Using None)

타입 힌트에서 사용될 때, 표현식 Nonetype(None)과 동일하게 간주됩니다.

타입 별칭 (Type aliases)

타입 별칭은 간단한 변수 할당으로 정의됩니다.

Url = str
def retry(url: Url, retry_count: int) -> None:
    ...

별칭 이름은 사용자 정의 타입(사용자 정의 클래스와 마찬가지로 일반적으로 대문자로 표기)을 나타내므로 대문자로 표기하는 것이 좋습니다.

타입 별칭은 어노테이션의 타입 힌트만큼 복잡할 수 있습니다. 타입 힌트로 허용되는 모든 것은 타입 별칭에서도 허용됩니다.

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)

def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)

vec = [] # type: Vector[float]

이는 다음 코드와 동일합니다.

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)

def inproduct(v: Iterable[Tuple[T, T]]) -> T:
    return sum(x*y for x, y in v)

def dilate(v: Iterable[Tuple[T, T]], scale: T) -> Iterable[Tuple[T, T]]:
    return ((x * scale, y * scale) for x, y in v)

vec = [] # type: Iterable[Tuple[float, float]]

Callable

특정 시그니처(signature)를 가진 콜백 함수를 기대하는 프레임워크는 Callable[[Arg1Type, Arg2Type], ReturnType]를 사용하여 타입 힌트를 지정할 수 있습니다. 예시:

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

인자 목록에 리터럴 생략 부호(세 점, ...)를 사용하여 호출 시그니처를 지정하지 않고 콜러블(callable)의 반환 타입을 선언할 수 있습니다.

def partial(func: Callable[..., str], *args) -> Callable[..., str]:
    # Body

생략 부호 주위에 대괄호가 없다는 점에 유의하십시오. 이 경우 콜백의 인자는 완전히 제약이 없습니다 (키워드 인자도 허용됩니다).

키워드 인자와 함께 콜백을 사용하는 것이 일반적인 사용 사례로 인식되지 않으므로, 현재 Callable과 함께 키워드 인자를 지정하는 기능은 지원되지 않습니다. 유사하게, 특정 타입의 가변 인자(variable number of arguments)로 콜백 시그니처를 지정하는 기능도 지원되지 않습니다.

typing.Callablecollections.abc.Callable의 대체 역할을 겸하므로, isinstance(x, typing.Callable)isinstance(x, collections.abc.Callable)로 위임하여 구현됩니다. 그러나 isinstance(x, typing.Callable[...])는 지원되지 않습니다.

제네릭 (Generics)

컨테이너에 저장된 객체에 대한 타입 정보는 제네릭 방식으로 정적으로 추론될 수 없으므로, 추상 베이스 클래스(abstract base classes)는 컨테이너 요소에 대한 예상 타입을 나타내기 위해 구독(subscription)을 지원하도록 확장되었습니다. 예시:

from typing import Mapping, Set

def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None:
    ...

제네릭은 typing에서 TypeVar라는 새로운 팩토리(factory)를 사용하여 매개변수화(parameterized)될 수 있습니다. 예시:

from typing import Sequence, TypeVar

T = TypeVar('T') # 타입 변수 선언

def first(l: Sequence[T]) -> T: # 제네릭 함수
    return l[0]

이 경우 계약(contract)은 반환된 값이 컬렉션이 보유한 요소와 일치한다는 것입니다.

TypeVar() 표현식은 항상 변수에 직접 할당되어야 합니다 (더 큰 표현식의 일부로 사용되어서는 안 됩니다). TypeVar()의 인자는 할당되는 변수 이름과 동일한 문자열이어야 합니다. 타입 변수는 재정의되어서는 안 됩니다.

TypeVar는 매개변수화된 타입(parametric types)을 고정된 가능한 타입 집합으로 제한하는 것을 지원합니다 (참고: 이러한 타입은 타입 변수로 매개변수화될 수 없습니다). 예를 들어, strbytes로만 범위가 지정되는 타입 변수를 정의할 수 있습니다. 기본적으로 타입 변수는 가능한 모든 타입에 걸쳐 범위가 지정됩니다. 타입 변수를 제한하는 예시:

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
    return x + y

함수 concat은 두 개의 str 인자 또는 두 개의 bytes 인자로 호출될 수 있지만, strbytes 인자를 혼합하여 호출할 수는 없습니다.

제약 조건이 있다면 최소한 두 개 이상이어야 합니다. 단일 제약 조건 지정은 허용되지 않습니다.

타입 변수에 의해 제약된 타입의 서브타입은 타입 변수의 컨텍스트에서 해당 명시적으로 나열된 기본 타입으로 처리되어야 합니다. 다음 예시를 고려하십시오.

class MyStr(str): ...
x = concat(MyStr('apple'), MyStr('pie'))

호출은 유효하지만 타입 변수 AnyStrMyStr이 아닌 str로 설정됩니다. 실제로 x에 할당된 반환 값의 추론된 타입도 str이 됩니다.

또한, Any는 모든 타입 변수에 대해 유효한 값입니다. 다음을 고려하십시오.

def count_truthy(elements: List[Any]) -> int:
    return sum(1 for elem in elements if elem)

이는 제네릭 표기법을 생략하고 단순히 elements: List라고 말하는 것과 동일합니다.

사용자 정의 제네릭 타입 (User-defined generic types)

Generic 베이스 클래스를 포함하여 사용자 정의 클래스를 제네릭으로 정의할 수 있습니다. 예시:

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('{}: {}'.format(self.name, message))

베이스 클래스로서의 Generic[T]LoggedVar 클래스가 단일 타입 매개변수 T를 취함을 정의합니다. 이것은 또한 T를 클래스 본문 내에서 유효한 타입으로 만듭니다.

Generic 베이스 클래스는 __getitem__을 정의하는 메타클래스를 사용하여 LoggedVar[t]가 유효한 타입이 되도록 합니다.

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

제네릭 타입은 원하는 수의 타입 변수를 가질 수 있으며, 타입 변수는 제약될 수 있습니다. 다음은 유효합니다.

from typing import TypeVar, Generic
# ...
T = TypeVar('T')
S = TypeVar('S')

class Pair(Generic[T, S]):
    ...

Generic에 대한 각 타입 변수 인자는 구별되어야 합니다. 따라서 다음은 유효하지 않습니다.

from typing import TypeVar, Generic
# ...
T = TypeVar('T')
class Pair(Generic[T, T]): # INVALID
    ...

다른 제네릭 클래스를 서브클래싱하고 해당 매개변수에 타입 변수를 지정하는 간단한 경우에는 Generic[T] 베이스 클래스가 중복됩니다.

from typing import TypeVar, Iterator

T = TypeVar('T')

class MyIter(Iterator[T]):
    ...

이 클래스 정의는 다음 코드와 동일합니다.

class MyIter(Iterator[T], Generic[T]):
    ...

Generic과 다중 상속을 사용할 수 있습니다.

from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

K = TypeVar('K')
V = TypeVar('V')

class MyMapping(Iterable[Tuple[K, V]], Container[Tuple[K, V]], Generic[K, V]):
    ...

타입 매개변수를 지정하지 않고 제네릭 클래스를 서브클래싱하면 각 위치에 Any를 가정합니다. 다음 예시에서 MyIterable은 제네릭이 아니지만 암묵적으로 Iterable[Any]를 상속합니다.

from typing import Iterable

class MyIterable(Iterable): # Same as Iterable[Any]
    ...

제네릭 메타클래스는 지원되지 않습니다.

타입 변수의 스코프 규칙 (Scoping rules for type variables)

타입 변수는 일반적인 이름 결정 규칙을 따릅니다. 그러나 정적 타입 검사 컨텍스트에는 몇 가지 특별한 경우가 있습니다.

제네릭 함수에서 사용되는 타입 변수는 동일한 코드 블록에서 다른 타입을 나타내는 것으로 추론될 수 있습니다. 예시:

from typing import TypeVar, Generic

T = TypeVar('T')

def fun_1(x: T) -> T: ... # T here
def fun_2(x: T) -> T: ... # and here could be different

fun_1(1)    # This is OK, T is inferred to be int
fun_2('a')  # This is also OK, now T is str

제네릭 클래스의 메서드에서 사용되는 타입 변수가 이 클래스를 매개변수화하는 변수 중 하나와 일치하는 경우, 항상 해당 변수에 바인딩됩니다. 예시:

from typing import TypeVar, Generic

T = TypeVar('T')

class MyClass(Generic[T]):
    def meth_1(self, x: T) -> T: ... # T here
    def meth_2(self, x: T) -> T: ... # and here are always the same

a = MyClass() # type: MyClass[int]
a.meth_1(1)   # OK
a.meth_2('a') # This is an error!

클래스를 매개변수화하는 변수 중 어느 것과도 일치하지 않는 메서드에서 사용되는 타입 변수는 해당 변수에서 이 메서드를 제네릭 함수로 만듭니다.

T = TypeVar('T')
S = TypeVar('S')

class Foo(Generic[T]):
    def method(self, x: T, y: S) -> S: ...

x = Foo() # type: Foo[int]
y = x.method(0, "abc") # inferred type of y is str

바인딩되지 않은 타입 변수(Unbound type variables)는 제네릭 함수의 본문이나 메서드 정의를 제외한 클래스 본문에 나타나서는 안 됩니다.

T = TypeVar('T')
S = TypeVar('S')

def a_fun(x: T) -> None: # this is OK
    y = [] # type: List[T]
    # but below is an error!
    y = [] # type: List[S]

class Bar(Generic[T]):
    # this is also an error
    an_attr = [] # type: List[S]

    def do_something(x: S) -> S: # this is OK though
        ...

제네릭 함수 내에 나타나는 제네릭 클래스 정의는 제네릭 함수를 매개변수화하는 타입 변수를 사용해서는 안 됩니다.

from typing import List

def a_fun(x: T) -> None:
    # This is OK
    a_list = [] # type: List[T]
    ...
    # This is however illegal
    class MyGeneric(Generic[T]):
        ...

다른 제네릭 클래스 내에 중첩된 제네릭 클래스는 동일한 타입 변수를 사용할 수 없습니다. 외부 클래스의 타입 변수 스코프는 내부 클래스를 포함하지 않습니다.

T = TypeVar('T')
S = TypeVar('S')

class Outer(Generic[T]):
    class Bad(Iterable[T]): # Error
        ...
    class AlsoBad:
        x = None # type: List[T] # Also an error
    class Inner(Iterable[S]): # OK
        ...
    attr = None # type: Inner[T] # Also OK

제네릭 클래스 인스턴스화 및 타입 소거 (Instantiating generic classes and type erasure)

사용자 정의 제네릭 클래스는 인스턴스화될 수 있습니다. Generic[T]를 상속하는 Node 클래스를 작성한다고 가정해 봅시다.

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    ...

Node 인스턴스를 생성하려면 일반 클래스와 마찬가지로 Node()를 호출합니다. 런타임에 인스턴스의 타입(클래스)은 Node가 됩니다. 하지만 타입 체커에게는 어떤 타입을 가질까요? 답변은 호출에서 사용 가능한 정보량에 따라 다릅니다. 생성자(__init__ 또는 __new__)가 시그니처에서 T를 사용하고 해당 인자 값이 전달되면, 해당 인자의 타입이 대체됩니다. 그렇지 않으면 Any로 가정됩니다. 예시:

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    x = None # type: T # Instance attribute (see below)
    def __init__(self, label: T = None) -> None:
        ...

x = Node('') # Inferred type is Node[str]
y = Node(0)  # Inferred type is Node[int]
z = Node()   # Inferred type is Node[Any]

추론된 타입이 [Any]를 사용하지만 의도된 타입이 더 구체적인 경우, 타입 주석(아래 참조)을 사용하여 변수의 타입을 강제할 수 있습니다. 예를 들면:

# (이전 예제에서 계속)
a = Node() # type: Node[int]
b = Node() # type: Node[str]

또는 특정 구체적인 타입으로 인스턴스화할 수 있습니다.

# (이전 예제에서 계속)
p = Node[int]()
q = Node[str]()
r = Node[int]('') # Error
s = Node[str](0) # Error

pq의 런타임 타입(클래스)은 여전히 Node일 뿐이라는 점에 유의하십시오. Node[int]Node[str]는 구별 가능한 클래스 객체이지만, 이를 인스턴스화하여 생성된 객체의 런타임 클래스는 구별을 기록하지 않습니다. 이 동작을 “타입 소거(type erasure)”라고 합니다. 이는 제네릭을 사용하는 언어(예: Java, TypeScript)에서 일반적인 관행입니다.

제네릭 클래스(매개변수화되었든 아니든)를 사용하여 속성에 액세스하면 타입 검사 실패가 발생합니다. 클래스 정의 본문 외부에서는 클래스 속성을 할당할 수 없으며, 동일한 이름의 인스턴스 속성이 없는 클래스 인스턴스를 통해서만 조회할 수 있습니다.

# (이전 예제에서 계속)
Node[int].x = 1 # Error
Node[int].x     # Error
Node.x = 1      # Error
Node.x          # Error
type(p).x       # Error
p.x             # Ok (evaluates to None)
Node[int]().x   # Ok (evaluates to None)
p.x = 1         # Ok, but assigning to instance attribute

Mapping 또는 Sequence와 같은 추상 컬렉션의 제네릭 버전과 List, Dict, Set, FrozenSet와 같은 내장 클래스의 제네릭 버전은 인스턴스화할 수 없습니다. 그러나 구체적인 사용자 정의 서브클래스와 구체적인 컬렉션의 제네릭 버전은 인스턴스화할 수 있습니다.

data = DefaultDict[int, bytes]()

정적 타입과 런타임 클래스를 혼동해서는 안 됩니다. 이 경우에도 타입은 소거되며, 위 표현식은 단순히 다음의 줄임말입니다.

data = collections.defaultdict() # type: DefaultDict[int, bytes]

하위 스크립트 클래스(예: Node[int])를 표현식에 직접 사용하는 것은 권장되지 않습니다. 대신 타입 별칭(예: IntNode = Node[int])을 사용하는 것이 선호됩니다. (첫째, 하위 스크립트 클래스를 생성하는 것, 즉 Node[int]는 런타임 비용이 있습니다. 둘째, 타입 별칭을 사용하는 것이 더 읽기 쉽습니다.)

임의의 제네릭 타입을 베이스 클래스로 (Arbitrary generic types as base classes)

Generic[T]는 베이스 클래스로만 유효하며, 적절한 타입이 아닙니다. 그러나 위 예시의 LinkedList[T]와 같은 사용자 정의 제네릭 타입, 그리고 List[T]Iterable[T]와 같은 내장 제네릭 타입 및 ABC는 타입과 베이스 클래스 모두로 유효합니다. 예를 들어, 타입 인자를 전문화하는 Dict의 서브클래스를 정의할 수 있습니다.

from typing import Dict, List, Optional

class Node: ...

class SymbolTable(Dict[str, List[Node]]):
    def push(self, name: str, node: Node) -> None:
        self.setdefault(name, []).append(node)
    def pop(self, name: str) -> Node:
        return self[name].pop()
    def lookup(self, name: str) -> Optional[Node]:
        nodes = self.get(name)
        if nodes:
            return nodes[-1]
        return None

SymbolTabledict의 서브클래스이자 Dict[str, List[Node]]의 서브타입입니다.

제네릭 베이스 클래스가 타입 인자로 타입 변수를 가지면, 정의된 클래스가 제네릭이 됩니다. 예를 들어, 이터러블(iterable)이자 컨테이너(container)인 제네릭 LinkedList 클래스를 정의할 수 있습니다.

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

class LinkedList(Iterable[T], Container[T]):
    ...

이제 LinkedList[int]는 유효한 타입입니다. Generic[...] 내에서 동일한 타입 변수 T를 여러 번 사용하지 않는 한, 베이스 클래스 목록에서 T를 여러 번 사용할 수 있다는 점에 유의하십시오.

다음 예시도 고려하십시오.

from typing import TypeVar, Mapping

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

이 경우 MyDict는 단일 매개변수 T를 가집니다.

추상 제네릭 타입 (Abstract generic types)

Generic에 사용되는 메타클래스는 abc.ABCMeta의 서브클래스입니다. 제네릭 클래스는 추상 메서드나 속성을 포함하여 ABC가 될 수 있으며, 제네릭 클래스는 메타클래스 충돌 없이 ABC를 베이스 클래스로 가질 수 있습니다.

상한을 가진 타입 변수 (Type variables with an upper bound)

타입 변수는 bound=<type>를 사용하여 상한(upper bound)을 지정할 수 있습니다 (참고: <type> 자체는 타입 변수로 매개변수화될 수 없습니다). 이는 타입 변수로 대체되는 실제 타입(명시적으로 또는 암묵적으로)이 경계 타입의 서브타입이어야 함을 의미합니다. 예시:

from typing import TypeVar, Sized

ST = TypeVar('ST', bound=Sized)

def longer(x: ST, y: ST) -> ST:
    if len(x) > len(y):
        return x
    else:
        return y

longer([1], [1, 2])    # ok, return type List[int]
longer({1}, {1, 2})    # ok, return type Set[int]
longer([1], {1, 2})    # ok, return type Collection[int]

상한은 타입 제약 조건(예: AnyStr 사용 참조)과 결합될 수 없습니다. 타입 제약 조건은 추론된 타입이 제약 조건 타입 중 정확히 하나가 되도록 하는 반면, 상한은 실제 타입이 경계 타입의 서브타입일 것을 요구할 뿐입니다.

공변성 및 반변성 (Covariance and contravariance)

Employee 클래스와 서브클래스 Manager를 고려해 봅시다. 이제 List[Employee]로 어노테이션된 인자를 가진 함수가 있다고 가정해 봅시다. 이 함수를 List[Manager] 타입의 변수를 인자로 사용하여 호출할 수 있어야 할까요? 많은 사람들은 결과는 고려하지 않고 “예, 물론이죠”라고 대답할 것입니다. 그러나 함수에 대해 더 많이 알지 못하는 한, 타입 체커는 그러한 호출을 거부해야 합니다. 함수가 목록에 Employee 인스턴스를 추가할 수 있으며, 이는 호출자의 변수 타입을 위반할 수 있기 때문입니다.

그러한 인자는 반변성(contravariantly)을 띠는 것으로 판명되었지만, 직관적인 답변(함수가 인자를 변경하지 않는 경우에는 올바른 답변입니다!)은 인자가 공변성(covariantly)을 띠도록 요구합니다. 이러한 개념에 대한 더 긴 소개는 위키백과와 PEP 483에서 찾을 수 있습니다. 여기서는 타입 체커의 동작을 제어하는 방법을 보여줄 뿐입니다.

기본적으로 제네릭 타입은 모든 타입 변수에서 불변성(invariant)을 띠는 것으로 간주됩니다. 이는 List[Employee]와 같은 타입으로 어노테이션된 변수의 값은 타입 어노테이션과 정확히 일치해야 함을 의미합니다. 타입 매개변수의 서브클래스 또는 슈퍼클래스(이 예에서는 Employee)는 허용되지 않습니다.

공변성 또는 반변성 타입 검사가 허용되는 컨테이너 타입을 선언하기 위해, 타입 변수는 키워드 인자 covariant=True 또는 contravariant=True를 허용합니다. 이 중 하나만 전달될 수 있습니다. 이러한 변수로 정의된 제네릭 타입은 해당 변수에 대해 공변성 또는 반변성으로 간주됩니다. 관례적으로 covariant=True로 정의된 타입 변수에는 _co로 끝나는 이름을, contravariant=True로 정의된 타입 변수에는 _contra로 끝나는 이름을 사용하는 것이 좋습니다.

일반적인 예는 불변(또는 읽기 전용) 컨테이너 클래스를 정의하는 것입니다.

from typing import TypeVar, Generic, Iterable, Iterator

T_co = TypeVar('T_co', covariant=True)

class ImmutableList(Generic[T_co]):
    def __init__(self, items: Iterable[T_co]) -> None: ...
    def __iter__(self) -> Iterator[T_co]: ...
    # ...

class Employee: ...
class Manager(Employee): ...

def dump_employees(emps: ImmutableList[Employee]) -> None:
    for emp in emps:
        ...

mgrs = ImmutableList([Manager()]) # type: ImmutableList[Manager]
dump_employees(mgrs) # OK

typing의 읽기 전용 컬렉션 클래스는 모두 타입 변수에서 공변성으로 선언됩니다 (예: MappingSequence). 변경 가능한 컬렉션 클래스(예: MutableMappingMutableSequence)는 불변성으로 선언됩니다. 반변성 타입의 한 가지 예는 Generator 타입으로, send() 인자 타입에서 반변성(contravariant)을 띠게 됩니다 (아래 참조).

참고: 공변성 또는 반변성은 타입 변수의 속성이 아니라, 이 변수를 사용하여 정의된 제네릭 클래스의 속성입니다. 분산(Variance)은 제네릭 타입에만 적용됩니다. 제네릭 함수는 이 속성을 가지지 않습니다. 후자는 covariant 또는 contravariant 키워드 인자 없이 타입 변수만을 사용하여 정의되어야 합니다. 예를 들어, 다음 예시는 괜찮습니다.

from typing import TypeVar

class Employee: ...
class Manager(Employee): ...

E = TypeVar('E', bound=Employee)

def dump_employee(e: E) -> None: ...

dump_employee(Manager()) # OK

반면 다음은 금지됩니다.

B_co = TypeVar('B_co', covariant=True)

def bad_func(x: B_co) -> B_co: # Flagged as error by a type checker
    ...

수치형 계층 구조 (The numeric tower)

PEP 3141은 Python의 수치형 계층 구조를 정의하며, 표준 라이브러리 모듈 numbers는 해당 ABC( Number, Complex, Real, Rational, Integral)를 구현합니다. 이 ABC들에는 몇 가지 문제가 있지만, 내장된 구체적인 수치 클래스 complex, float, int는 어디에나 있습니다 (특히 후자 두 가지).

사용자가 import numbers를 작성한 다음 numbers.Float 등을 사용하도록 요구하는 대신, 이 PEP는 거의 동일하게 효과적인 간단한 단축키를 제안합니다. 즉, 인자가 float 타입으로 어노테이션되면 int 타입의 인자가 허용됩니다. 유사하게, complex 타입으로 어노테이션된 인자의 경우, float 또는 int 타입의 인자가 허용됩니다. 이는 해당 ABC를 구현하는 클래스나 fractions.Fraction 클래스를 처리하지 않지만, 이러한 사용 사례는 극히 드물다고 생각합니다.

선언되지 않은 이름 참조 (Forward references)

타입 힌트에 아직 정의되지 않은 이름이 포함될 때, 해당 정의는 나중에 해결될 문자열 리터럴로 표현될 수 있습니다.

이것이 일반적으로 발생하는 상황은 컨테이너 클래스의 정의로, 정의되는 클래스가 일부 메서드의 시그니처에 나타나는 경우입니다. 예를 들어, 다음 코드(간단한 이진 트리 구현의 시작)는 작동하지 않습니다.

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

이를 해결하기 위해 다음과 같이 작성합니다.

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

문자열 리터럴은 유효한 Python 표현식(즉, compile(lit, '', 'eval')이 유효한 코드 객체여야 함)을 포함해야 하며, 모듈이 완전히 로드된 후 오류 없이 평가되어야 합니다. 평가되는 로컬(local) 및 전역(global) 네임스페이스는 동일한 함수의 기본 인수가 평가되는 네임스페이스와 동일해야 합니다.

더욱이, 이 표현식은 유효한 타입 힌트로 파싱될 수 있어야 합니다. 즉, 위 섹션 “허용되는 타입 힌트”의 규칙에 의해 제약됩니다.

타입 힌트의 일부로 문자열 리터럴을 사용하는 것은 허용됩니다. 예를 들면:

class Tree: ...
def leaves(self) -> List['Tree']: ...

선언되지 않은 이름 참조의 일반적인 사용 사례는 예를 들어 Django 모델이 시그니처에 필요할 때입니다. 일반적으로 각 모델은 별도의 파일에 있으며, 다른 모델을 포함하는 타입의 인자를 받는 메서드를 가집니다. Python에서 순환 임포트(circular imports)가 작동하는 방식 때문에 필요한 모든 모델을 직접 임포트하는 것이 종종 불가능합니다.

# File models/a.py
from models.b import B

class A(Model):
    def foo(self, b: B): ...

# File models/b.py
from models.a import A

class B(Model):
    def bar(self, a: A): ...

# File main.py
from models.a import A
from models.b import B

main이 먼저 임포트된다고 가정하면, models/a.py에서 A 클래스가 정의되기 전에 models/b.py에서 models.a를 임포트하는 줄에서 ImportError가 발생하여 실패할 것입니다. 해결책은 모듈 전용 임포트로 전환하고 모델을 _module_._class_ 이름으로 참조하는 것입니다.

# File models/a.py
from models import b

class A(Model):
    def foo(self, b: 'b.B'): ...

# File models/b.py
from models import a

class B(Model):
    def bar(self, a: 'a.A'): ...

# File main.py
from models.a import A
from models.b import B

유니언 타입 (Union types)

단일 인자에 대해 작고 제한된 예상 타입 집합을 허용하는 것이 일반적이므로, Union이라는 새로운 특수 팩토리가 있습니다. 예시:

from typing import Union

def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
    if isinstance(e, Employee):
        e = [e]
    ...

Union[T1, T2, ...]에 의해 생성된 타입은 모든 타입 T1, T2 등의 슈퍼타입이므로, 이러한 타입 중 하나의 멤버인 값은 Union[T1, T2, ...]로 어노테이션된 인자에 대해 허용됩니다.

유니언 타입의 일반적인 경우는 옵셔널 타입(optional types)입니다. 기본적으로 None은 함수 정의에 None의 기본값이 제공되지 않는 한 어떤 타입에 대해서도 유효하지 않은 값입니다. 예시:

def handle_employee(e: Union[Employee, None]) -> None:
    ...

Union[T1, None]의 약어로 Optional[T1]를 작성할 수 있습니다. 예를 들어, 위 코드는 다음 코드와 동일합니다.

from typing import Optional

def handle_employee(e: Optional[Employee]) -> None:
    ...

이 PEP의 이전 버전에서는 기본값이 None일 때 타입 체커가 옵셔널 타입을 가정하도록 허용했습니다.

def handle_employee(e: Employee = None): ...

이는 다음 코드와 동일하게 처리되었을 것입니다.

def handle_employee(e: Optional[Employee] = None) -> None: ...

이것은 더 이상 권장되는 동작이 아닙니다. 타입 체커는 옵셔널 타입을 명시적으로 요구하는 방향으로 나아가야 합니다.

유니언 내 싱글톤 타입 지원 (Support for singleton types in unions)

싱글톤 인스턴스는 특정 특수 조건을 표시하는 데 자주 사용됩니다. 특히 None도 변수의 유효한 값인 상황에서 그렇습니다. 예시:

_empty = object()

def func(x=_empty):
    if x is _empty: # default argument value
        return 0
    elif x is None: # argument was provided and it's None
        return 1
    else:
        return x * 2

이러한 상황에서 정확한 타이핑을 허용하기 위해, 사용자는 표준 라이브러리에서 제공하는 enum.Enum 클래스와 함께 Union 타입을 사용하여 타입 오류를 정적으로 catch할 수 있습니다.

from typing import Union
from enum import Enum

class Empty(Enum):
    token = 0

_empty = Empty.token

def func(x: Union[int, None, Empty] = _empty) -> int:
    boom = x * 42 # This fails type check
    if x is _empty:
        return 0
    elif x is None:
        return 1
    else:
        # At this point typechecker knows that x can only have type int
        return x * 2

Enum의 서브클래스는 더 이상 서브클래싱될 수 없으므로, 위 예시의 모든 분기에서 변수 x의 타입을 정적으로 추론할 수 있습니다. 둘 이상의 싱글톤 객체가 필요한 경우에도 동일한 접근 방식이 적용됩니다. 둘 이상의 값을 가진 열거형을 사용할 수 있습니다.

class Reason(Enum):
    timeout = 1
    error = 2

def process(response: Union[str, Reason] = '') -> str:
    if response is Reason.timeout:
        return 'TIMEOUT'
    elif response is Reason.error:
        return 'ERROR'
    else:
        # response can be only str, all other possible values exhausted
        return 'PROCESSED: ' + response

Any 타입 (The Any type)

특별한 타입 중 하나는 Any입니다. 모든 타입은 Any와 일관됩니다. 이는 모든 값과 모든 메서드를 가진 타입으로 간주될 수 있습니다. Any와 내장 타입 object는 완전히 다르다는 점에 유의하십시오.

값의 타입이 object인 경우, 타입 체커는 해당 값에 대한 거의 모든 작업을 거부하며, 이를 더 구체적인 타입의 변수에 할당하거나(또는 반환 값으로 사용하거나)하는 것은 타입 오류입니다. 반면에 값이 Any 타입인 경우, 타입 체커는 해당 값에 대한 모든 작업을 허용하며, Any 타입의 값은 더 제약된 타입의 변수에 할당될 수 있습니다(또는 반환 값으로 사용될 수 있습니다).

어노테이션이 없는 함수 매개변수는 Any로 어노테이션된 것으로 가정됩니다. 제네릭 타입이 타입 매개변수를 지정하지 않고 사용되면 Any로 가정됩니다.

from typing import Mapping

def use_map(m: Mapping) -> None: # Same as Mapping[Any, Any]
    ...

이 규칙은 Tuple에도 적용되며, 어노테이션 컨텍스트에서는 Tuple[Any, ...]와 동일하고, 다시 tuple과 동일합니다. 마찬가지로, 어노테이션의 bare CallableCallable[..., Any]와 동일하고, 다시 collections.abc.Callable과 동일합니다.

from typing import Tuple, List, Callable

def check_args(args: Tuple) -> bool: ...
check_args(())          # OK
check_args((42, 'abc')) # Also OK
check_args(3.14)        # Flagged as error by a type checker

# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None: ...

NoReturn 타입 (The NoReturn type)

typing 모듈은 정상적으로 반환되지 않는 함수를 어노테이션하기 위한 특수 타입 NoReturn을 제공합니다. 예를 들어, 무조건 예외를 발생시키는 함수는 다음과 같습니다.

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

NoReturn 어노테이션은 sys.exit와 같은 함수에 사용됩니다. 정적 타입 체커는 NoReturn을 반환한다고 어노테이션된 함수가 암묵적으로든 명시적으로든 절대로 반환하지 않도록 보장합니다.

import sys
from typing import NoReturn

def f(x: int) -> NoReturn: # Error, f(0) implicitly returns None
    if x != 0:
        sys.exit(1)

체커는 또한 그러한 함수 호출 이후의 코드가 도달할 수 없다는 것을 인식하고 그에 따라 동작할 것입니다.

# continue from first example
def g(x: int) -> int:
    if x > 0:
        return x
    stop()
    return 'whatever works' # Error might be not reported by some checkers
                            # that ignore errors in unreachable blocks

NoReturn 타입은 함수의 반환 어노테이션으로만 유효하며, 다른 위치에 나타나면 오류로 간주됩니다.

from typing import List, NoReturn

# All of the following are errors
def bad1(x: NoReturn) -> int: ...
bad2 = None # type: NoReturn
def bad3() -> List[NoReturn]: ...

클래스 객체의 타입 (The type of class objects)

때로는 클래스 객체, 특히 주어진 클래스를 상속하는 클래스 객체에 대해 이야기하고 싶을 때가 있습니다. 이것은 C가 클래스일 때 Type[C]로 표현될 수 있습니다. 명확히 하자면, C (어노테이션으로 사용될 때)가 클래스 C의 인스턴스를 참조하는 반면, Type[C]C의 서브클래스를 참조합니다. (이는 objecttype 사이의 유사한 구별입니다.)

예를 들어, 다음과 같은 클래스들이 있다고 가정해 봅시다.

class User: ... # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

그리고 클래스 객체를 전달하면 이러한 클래스 중 하나의 인스턴스를 생성하는 함수가 있다고 가정해 봅시다.

def new_user(user_class):
    user = user_class()
    # (Here we could write the user object to a database)
    return user

Type[] 없이 new_user()를 어노테이션하는 최선은 다음과 같을 것입니다.

def new_user(user_class: type) -> User: ...

그러나 Type[]와 상한(upper bound)을 가진 타입 변수를 사용하면 훨씬 더 나은 작업을 수행할 수 있습니다.

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U: ...

이제 new_user()User의 특정 서브클래스로 호출하면 타입 체커는 결과의 올바른 타입을 추론할 것입니다.

joe = new_user(BasicUser) # Inferred type is BasicUser

Type[C]에 해당하는 값은 C의 서브타입인 실제 클래스 객체여야 하며, 특수 형식이 아닙니다. 즉, 위 예시에서 new_user(Union[BasicUser, ProUser])와 같은 호출은 타입 체커에 의해 거부됩니다 (유니언을 인스턴스화할 수 없으므로 런타임에도 실패함).

다음과 같이 Type[]의 매개변수로 클래스 유니언을 사용하는 것은 유효합니다.

def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
    user = new_user(user_class)
    ...

그러나 런타임에 전달되는 실제 인수는 여전히 구체적인 클래스 객체여야 합니다. 예를 들어, 위 예시에서:

new_non_team_user(ProUser)   # OK
new_non_team_user(TeamUser)  # Disallowed by type checker

Type[Any]도 지원됩니다 (그 의미는 아래 참조).

T가 타입 변수인 Type[T]는 클래스 메서드의 첫 번째 인자를 어노테이션할 때 허용됩니다 (관련 섹션 참조).

Tuple 또는 Callable과 같은 다른 특수 구문은 Type의 인자로 허용되지 않습니다.

이 기능에는 몇 가지 우려 사항이 있습니다. 예를 들어 new_user()user_class()를 호출할 때, 이는 User의 모든 서브클래스가 생성자 시그니처에서 이를 지원해야 함을 의미합니다. 그러나 이것은 Type[]에만 해당되는 것이 아닙니다. 클래스 메서드도 유사한 우려 사항을 가집니다. 타입 체커는 이러한 가정의 위반을 표시해야 하지만, 기본적으로 표시된 베이스 클래스(위 예시에서는 User)의 생성자 시그니처와 일치하는 생성자 호출은 허용되어야 합니다. 복잡하거나 확장 가능한 클래스 계층을 포함하는 프로그램은 팩토리 클래스 메서드를 사용하여 이를 처리할 수도 있습니다. 이 PEP의 향후 개정판에서는 이러한 우려 사항을 처리하는 더 나은 방법을 도입할 수 있습니다.

Type이 매개변수화될 때 정확히 하나의 매개변수를 요구합니다. 괄호 없는 일반 TypeType[Any]와 동일하며, 이는 다시 type(Python의 메타클래스 계층 구조의 루트)과 동일합니다. 이러한 동등성은 이 기능이 논의되는 동안 제안되었던 Class 또는 SubType와 같은 대안이 아닌 Type이라는 이름을 부여하는 동기가 됩니다. 이는 Listlist 사이의 관계와 유사합니다.

Type[Any] (또는 Type 또는 type)의 동작과 관련하여, 이 타입의 변수 속성에 액세스하면 type에 의해 정의된 속성 및 메서드만 제공합니다 (예: __repr__()__mro__). 이러한 변수는 임의의 인수로 호출될 수 있으며, 반환 타입은 Any입니다.

Type은 해당 매개변수에서 공변성(covariant)을 띠는데, 이는 Type[Derived]Type[Base]의 서브타입이기 때문입니다.

def new_pro_user(pro_user_class: Type[ProUser]):
    user = new_user(pro_user_class) # OK
    ...

인스턴스 및 클래스 메서드 어노테이션 (Annotating instance and class methods)

대부분의 경우 클래스 및 인스턴스 메서드의 첫 번째 인수는 어노테이션할 필요가 없으며, 인스턴스 메서드의 경우 포함하는 클래스의 타입, 클래스 메서드의 경우 포함하는 클래스 객체에 해당하는 타입 객체 타입으로 가정됩니다. 또한, 인스턴스 메서드의 첫 번째 인수는 타입 변수로 어노테이션될 수 있습니다. 이 경우 반환 타입은 동일한 타입 변수를 사용할 수 있으며, 이로 인해 해당 메서드가 제네릭 함수가 됩니다. 예를 들면:

T = TypeVar('T', bound='Copyable')

class Copyable:
    def copy(self: T) -> T: # return a copy of self
        ...

class C(Copyable):
    ...

c = C()
c2 = c.copy() # type here should be C

클래스 메서드의 첫 번째 인자 어노테이션에서 Type[]를 사용하는 경우에도 마찬가지입니다.

T = TypeVar('T', bound='C')

class C:
    @classmethod
    def factory(cls: Type[T]) -> T: # make a new instance of cls
        ...

class D(C):
    ...

d = D.factory() # type here should be D

일부 타입 체커는 이 사용에 제한을 가할 수 있으며, 사용된 타입 변수에 적절한 상한(upper bound)을 요구할 수 있다는 점에 유의하십시오 (예시 참조).

버전 및 플랫폼 확인 (Version and platform checking)

타입 체커는 간단한 버전 및 플랫폼 확인을 이해할 것으로 예상됩니다. 예를 들면:

import sys

if sys.version_info[0] >= 3:
    # Python 3 specific definitions
else:
    # Python 2 specific definitions

if sys.platform == 'win32':
    # Windows specific definitions
else:
    # Posix specific definitions

체커가 "".join(reversed(sys.platform)) == "xunil"과 같은 난독화를 이해할 것으로 기대하지 마십시오.

런타임 또는 타입 검사 (Runtime or type checking?)

때로는 타입 체커(또는 다른 정적 분석 도구)가 봐야 하지만 실행되어서는 안 되는 코드가 있습니다. 이러한 상황을 위해 typing 모듈은 TYPE_CHECKING이라는 상수를 정의합니다. 이 상수는 타입 검사(또는 다른 정적 분석) 중에는 True로 간주되지만 런타임에는 False입니다. 예시:

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: 'expensive_mod.SomeClass') -> None:
    a_var = arg # type: expensive_mod.SomeClass
    ...

(타입 어노테이션은 따옴표로 묶어 “선언되지 않은 이름 참조(forward reference)”로 만들어 expensive_mod 참조를 인터프리터 런타임에서 숨겨야 합니다. # type 주석에서는 따옴표가 필요하지 않습니다.)

이 접근 방식은 임포트 주기(import cycles)를 처리하는 데도 유용할 수 있습니다.

임의 인자 목록 및 기본 인자 값 (Arbitrary argument lists and default argument values)

임의 인자 목록도 타입 어노테이션될 수 있습니다. 다음 정의는 허용됩니다.

def foo(*args: str, **kwds: int):
    ...

이는 예를 들어 다음 함수 호출이 모두 유효한 타입의 인자를 나타냄을 의미합니다.

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

함수 foo의 본문에서 변수 args의 타입은 Tuple[str, ...]로, 변수 kwds의 타입은 Dict[str, int]로 추론됩니다.

스텁 파일에서는 실제 기본값을 지정하지 않고 인자가 기본값을 가진다고 선언하는 것이 유용할 수 있습니다. 예를 들면:

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr:
    ...

기본값은 어떤 모습이어야 할까요? "", b"", None 중 어떤 것도 타입 제약 조건을 만족하지 못합니다.

이러한 경우 기본값은 리터럴 생략 부호(ellipsis)로 지정될 수 있습니다. 즉, 위 예시는 말 그대로 작성해야 하는 것입니다.

위치 전용 인자 (Positional-only arguments)

일부 함수는 인자를 위치(positionally)로만 받도록 설계되었으며, 호출자가 인자 이름을 사용하여 키워드(keyword)로 해당 인자를 제공하지 않을 것을 기대합니다. 이름이 __로 시작하는 모든 인자는 위치 전용으로 가정되지만, 이름이 __로 끝나지 않는 경우는 예외입니다.

def quux(__x: int, __y__: int = 0) -> None:
    ...

quux(3, __y__=1) # This call is fine.
quux(__x=3)      # This call is an error.

제너레이터 함수 및 코루틴 어노테이션 (Annotating generator functions and coroutines)

제너레이터 함수의 반환 타입은 typing.py 모듈에서 제공하는 제네릭 타입 Generator[yield_type, send_type, return_type]으로 어노테이션될 수 있습니다.

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

PEP 492에서 도입된 코루틴(coroutines)은 일반 함수와 동일한 문법으로 어노테이션됩니다. 그러나 반환 타입 어노테이션은 코루틴 타입이 아닌 await 표현식의 타입에 해당합니다.

async def spam(ignored: int) -> str:
    return 'spam'

async def foo() -> None:
    bar = await spam(42) # type: str

typing.py 모듈은 collections.abc.Coroutine의 제네릭 버전을 제공하여 send()throw() 메서드도 지원하는 awaitable을 지정합니다. 타입 변수의 분산(variance) 및 순서는 Generator의 것과 일치합니다. 즉 Coroutine[T_co, T_contra, V_co]입니다. 예를 들면:

from typing import List, Coroutine

c = None # type: Coroutine[List[str], str, int]
...
x = c.send('hi') # type: List[str]

async def bar() -> None:
    x = await c # type: int

이 모듈은 또한 보다 정확한 타입을 지정할 수 없는 상황을 위해 제네릭 ABC Awaitable, AsyncIterable, AsyncIterator를 제공합니다.

def op() -> typing.Awaitable[str]:
    if cond:
        return spam(42)
    else:
        return asyncio.Future(...)

함수 어노테이션의 다른 사용과의 호환성 (Compatibility with other uses of function annotations)

타입 힌트와 호환되지 않는 함수 어노테이션에 대한 여러 기존 또는 잠재적 사용 사례가 존재합니다. 이들은 정적 타입 체커를 혼동시킬 수 있습니다. 그러나 타입 힌트 어노테이션은 런타임 동작이 없으므로(어노테이션 표현식 평가 및 함수 객체의 __annotations__ 속성에 어노테이션 저장 외에는), 프로그램이 잘못되지 않습니다. 단지 타입 체커가 잘못된 경고나 오류를 발생시킬 수 있습니다.

타입 힌팅에 포함되어서는 안 되는 프로그램 부분을 표시하려면 다음 중 하나 이상을 사용할 수 있습니다.

  • # type: ignore 주석
  • 클래스 또는 함수에 @no_type_check 데코레이터
  • @no_type_check_decorator로 표시된 사용자 정의 클래스 또는 함수 데코레이터

자세한 내용은 뒷부분 섹션을 참조하십시오.

오프라인 타입 검사와의 최대 호환성을 위해, 어노테이션에 의존하는 인터페이스를 다른 메커니즘(예: 데코레이터)으로 전환하는 것이 궁극적으로 좋은 생각일 수 있습니다. 그러나 Python 3.5에서는 이를 강제할 필요가 없습니다. 아래 “거부된 대안(Rejected Alternatives)” 섹션의 더 긴 논의도 참조하십시오.

타입 주석 (Type comments)

이 PEP에서는 변수를 특정 타입으로 명시적으로 표시하기 위한 일급(first-class) 문법 지원이 추가되지 않습니다. 복잡한 경우 타입 추론을 돕기 위해 다음 형식의 주석을 사용할 수 있습니다.

x = [] # type: List[Employee]
x, y, z = [], [], [] # type: List[int], List[int], List[str]
x, y, z = [], [], [] # type: (List[int], List[int], List[str])
a, b, *c = range(5) # type: float, float, List[float]
x = [1, 2] # type: List[int]

타입 주석은 변수 정의를 포함하는 문장의 마지막 줄에 있어야 합니다. with 문과 for 문에도 콜론 바로 뒤에 배치될 수 있습니다.

withfor 문에 대한 타입 주석 예시:

with frobnicate() as foo: # type: int
    # Here foo is an int
    ...
for x, y in points: # type: float, float
    # Here x and y are floats
    ...

스텁 파일에서는 변수의 초기값을 지정하지 않고 변수의 존재를 선언하는 것이 유용할 수 있습니다. 이는 PEP 526 변수 어노테이션 문법을 사용하여 수행할 수 있습니다.

from typing import IO

stream: IO[str]

위 문법은 모든 Python 버전의 스텁 파일에서 허용됩니다. 그러나 Python 3.5 및 이전 버전의 비스텁 코드에서는 특별한 경우가 있습니다.

from typing import IO

stream = None # type: IO[str]

타입 체커는 이에 대해 불평해서는 안 되며(주어진 타입과 None 값이 일치하지 않더라도), 추론된 타입을 Optional[...]로 변경해서도 안 됩니다(기본값이 None인 어노테이션된 인수에 대해 이를 수행하는 규칙이 있더라도). 여기에서 가정하는 것은 다른 코드가 변수에 적절한 타입의 값을 부여하도록 보장하며, 모든 사용은 변수가 주어진 타입을 가진다고 가정할 수 있다는 것입니다.

# type: ignore 주석은 오류가 참조하는 줄에 배치되어야 합니다.

import http.client

errors = {
    'not_found': http.client.NOT_FOUND # type: ignore
}

파일 맨 위에, 어떤 독스트링(docstrings), 임포트 또는 기타 실행 가능한 코드보다 먼저, 단독으로 놓인 # type: ignore 주석은 파일의 모든 오류를 억제합니다. 빈 줄과 기타 주석(예: shebang 줄 및 코딩 쿠키)은 # type: ignore 주석보다 앞에 올 수 있습니다.

어떤 경우에는 린팅(linting) 도구 또는 다른 주석이 타입 주석과 같은 줄에 필요할 수 있습니다. 이러한 경우 타입 주석은 다른 주석 및 린팅 마커보다 먼저 와야 합니다.

# type: ignore # <comment or other marker>

타입 힌팅이 일반적으로 유용하다고 판명되면, 변수 타이핑을 위한 문법이 향후 Python 버전에 제공될 수 있습니다. (업데이트: 이 문법은 PEP 526을 통해 Python 3.6에 추가되었습니다.)

캐스트 (Casts)

가끔 타입 체커는 다른 종류의 힌트가 필요할 수 있습니다. 프로그래머는 표현식이 타입 체커가 추론할 수 있는 것보다 더 제약된 타입임을 알고 있을 수 있습니다. 예를 들면:

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string in a
    return cast(str, a[index])

일부 타입 체커는 a[index]의 타입이 str임을 추론하지 못하고 object 또는 Any로만 추론할 수 있지만, 우리는 (코드가 그 지점에 도달하면) str이어야 한다는 것을 알고 있습니다. cast(t, x) 호출은 타입 체커에게 x의 타입이 t라고 확신한다고 알려줍니다. 런타임에 cast는 항상 표현식을 변경하지 않고 반환합니다. 타입 검사를 하지 않고, 값을 변환하거나 강제하지 않습니다.

캐스트는 타입 주석과 다릅니다 (이전 섹션 참조). 타입 주석을 사용할 때 타입 체커는 추론된 타입이 명시된 타입과 일치하는지 여전히 확인해야 합니다. 캐스트를 사용할 때 타입 체커는 프로그래머를 맹목적으로 믿어야 합니다. 또한, 캐스트는 표현식에 사용될 수 있지만, 타입 주석은 할당에만 적용됩니다.

NewType 도우미 함수 (NewType helper function)

프로그래머가 간단한 클래스를 생성하여 논리적 오류를 피하고 싶을 때도 있습니다. 예를 들면:

class UserId(int): pass

def get_by_user_id(user_id: UserId): ...

그러나 이 접근 방식은 런타임 오버헤드를 발생시킵니다. 이를 피하기 위해 typing.py는 런타임 오버헤드가 거의 없는 간단하고 고유한 타입을 생성하는 도우미 함수 NewType을 제공합니다. 정적 타입 체커의 경우 Derived = NewType('Derived', Base)는 대략 다음 정의와 동일합니다.

class Derived(Base):
    def __init__(self, _x: Base) -> None: ...

런타임에는 NewType('Derived', Base)가 단순히 인자를 반환하는 더미 함수를 반환합니다. 타입 체커는 UserId가 예상되는 곳에서 int로부터 명시적 캐스트를 요구하는 반면, int가 예상되는 곳에서는 UserId로부터 암묵적으로 캐스트합니다. 예시:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str: ...

UserId('user')        # Fails type check
name_by_id(42)        # Fails type check
name_by_id(UserId(42)) # OK

num = UserId(5) + 1   # type: int

NewType은 정확히 두 개의 인자를 받습니다. 새 고유 타입의 이름과 기본 클래스입니다. 후자는 적절한 클래스여야 합니다 (즉, Union 등과 같은 타입 구문이 아니어야 함), 또는 NewType을 호출하여 생성된 다른 고유 타입이어야 합니다. NewType이 반환하는 함수는 하나의 인자만 받습니다. 이는 기본 클래스의 인스턴스를 받는 하나의 생성자만 지원하는 것과 동일합니다 (위 참조). 예시:

class PacketId:
    def __init__(self, major: int, minor: int) -> None:
        self._major = major
        self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet)       # OK
tcp_packet = TcpPacketId(127, 0)       # Fails in type checker and at runtime

isinstanceissubclass 모두, 그리고 서브클래싱은 함수 객체가 이러한 연산을 지원하지 않으므로 NewType('Derived', Base)에 대해 실패할 것입니다.

스텁 파일 (Stub Files)

스텁 파일은 런타임에 사용되지 않고 타입 체커에서만 사용되는 타입 힌트를 포함하는 파일입니다. 스텁 파일에 대한 몇 가지 사용 사례가 있습니다.

  • 확장 모듈
  • 작성자가 아직 타입 힌트를 추가하지 않은 서드파티 모듈
  • 타입 힌트가 아직 작성되지 않은 표준 라이브러리 모듈
  • Python 2와 3 모두와 호환되어야 하는 모듈
  • 다른 목적으로 어노테이션을 사용하는 모듈

스텁 파일은 일반 Python 모듈과 동일한 문법을 가집니다. typing 모듈에는 스텁 파일에서 다른 하나의 기능이 있습니다: 아래 설명된 @overload 데코레이터입니다.

타입 체커는 스텁 파일의 함수 시그니처만 검사해야 합니다. 스텁 파일의 함수 본문은 단일 생략 부호(...)로만 구성하는 것이 권장됩니다.

타입 체커는 스텁 파일에 대한 구성 가능한 검색 경로를 가져야 합니다. 스텁 파일이 발견되면 타입 체커는 해당 “실제” 모듈을 읽어서는 안 됩니다.

스텁 파일은 구문적으로 유효한 Python 모듈이지만, 해당 실제 모듈과 동일한 디렉토리에 스텁 파일을 유지할 수 있도록 .pyi 확장자를 사용합니다. 이는 또한 스텁 파일에서 런타임 동작이 예상되어서는 안 된다는 개념을 강화합니다.

스텁 파일에 대한 추가 참고 사항:

  • 스텁으로 임포트된 모듈 및 변수는 임포트가 import ... as ... 형식 또는 이와 동등한 from ... import ... as ... 형식을 사용하지 않는 한 스텁에서 내보내진 것으로 간주되지 않습니다. (업데이트: 명확히 하자면, 여기서 의도는 X as X 형식으로 임포트된 이름만 내보내진다는 것입니다. 즉, as 이전과 이후의 이름이 동일해야 합니다.) 그러나 이전 항목의 예외로, from ... import *를 사용하여 스텁으로 임포트된 모든 객체는 내보내진 것으로 간주됩니다. (이것은 Python 버전에 따라 달라질 수 있는 특정 모듈의 모든 객체를 다시 내보내는 것을 더 쉽게 만듭니다.)
  • 일반 Python 파일과 마찬가지로, 서브모듈은 임포트될 때 자동으로 상위 모듈의 내보내진 속성이 됩니다. 예를 들어, spam 패키지가 다음과 같은 디렉토리 구조를 가지고 있다면:

    spam/
        __init__.pyi
        ham.pyi
    

    여기서 __init__.pyifrom . import ham 또는 from .ham import Ham과 같은 줄이 포함되어 있다면, hamspam의 내보내진 속성입니다.

  • 스텁 파일은 불완전할 수 있습니다. 타입 체커에게 이를 알리기 위해 파일은 다음 코드를 포함할 수 있습니다.

    def __getattr__(name) -> Any: ...
    

    따라서 스텁에 정의되지 않은 모든 식별자는 Any 타입으로 가정됩니다.

함수/메서드 오버로딩 (Function/method overloading)

@overload 데코레이터는 여러 다른 인자 타입 조합을 지원하는 함수 및 메서드를 설명할 수 있도록 합니다. 이 패턴은 내장 모듈 및 타입에서 자주 사용됩니다. 예를 들어, bytes 타입의 __getitem__() 메서드는 다음과 같이 설명될 수 있습니다.

from typing import overload

class bytes:
    ...
    @overload
    def __getitem__(self, i: int) -> int: ...
    @overload
    def __getitem__(self, s: slice) -> bytes: ...

이 설명은 유니언을 사용하여 가능했을 것보다 더 정확합니다 (유니언은 인자와 반환 타입 사이의 관계를 표현할 수 없습니다).

from typing import Union

class bytes:
    ...
    def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ...

@overload가 유용한 또 다른 예는 내장 함수 map()의 타입으로, 콜러블의 타입에 따라 다른 수의 인자를 받습니다.

from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload

T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')

@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S], iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables

map(None, ...)을 지원하기 위해 항목을 쉽게 추가할 수도 있습니다.

@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]: ...
@overload
def map(func: None, iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]: ...

위와 같이 @overload 데코레이터를 사용하는 것은 스텁 파일에 적합합니다. 일반 모듈에서는 @overload 데코레이터가 붙은 일련의 정의 다음에 정확히 하나의 @overload 데코레이터가 붙지 않은 정의(동일한 함수/메서드에 대한)가 와야 합니다. @overload 데코레이터가 붙은 정의는 타입 체커만을 위한 것이며, @overload 데코레이터가 붙지 않은 정의에 의해 덮어쓰여질 것이기 때문입니다. 후자는 런타임에 사용되지만 타입 체커에 의해 무시되어야 합니다. 런타임에 @overload 데코레이터가 붙은 함수를 직접 호출하면 NotImplementedError가 발생합니다. 다음은 유니언이나 타입 변수를 사용하여 쉽게 표현할 수 없는 비스텁 오버로드의 예입니다.

@overload
def utf8(value: None) -> None: pass
@overload
def utf8(value: bytes) -> bytes: pass
@overload
def utf8(value: unicode) -> bytes: pass
def utf8(value):
    <actual implementation>

참고: 이 문법을 사용하여 다중 디스패치(multiple dispatch) 구현을 제공할 수도 있지만, 그 구현은 sys._getframe()을 사용해야 하므로 권장되지 않습니다. 또한 효율적인 다중 디스패치 메커니즘을 설계하고 구현하는 것은 어려우므로, 이전 시도는 functools.singledispatch()를 선호하여 포기되었습니다. (PEP 443, 특히 “대안적 접근 방식” 섹션 참조). 미래에 만족스러운 다중 디스패치 설계를 제시할 수도 있지만, 스텁 파일의 타입 힌트에 정의된 오버로딩 문법에 의해 그러한 설계가 제약되는 것을 원하지 않습니다. 또한 두 기능이 서로 독립적으로 발전할 수도 있습니다 (타입 체커의 오버로딩은 런타임의 다중 디스패치와는 다른 사용 사례 및 요구 사항을 가지기 때문입니다. 예를 들어, 후자는 제네릭 타입을 지원하지 않을 가능성이 높습니다).

@overload 데코레이터를 사용하는 대신 제약된 TypeVar 타입을 자주 사용할 수 있습니다. 예를 들어, 이 스텁 파일의 concat1concat2 정의는 동일합니다.

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ...

@overload
def concat2(x: str, y: str) -> str: ...
@overload
def concat2(x: bytes, y: bytes) -> bytes: ...

map 또는 bytes.__getitem__과 같은 일부 함수는 타입 변수를 사용하여 정확하게 표현할 수 없습니다. 그러나 @overload와 달리 타입 변수는 스텁 파일 외부에서도 사용할 수 있습니다. @overload는 특수 스텁 전용 상태 때문에 타입 변수가 충분하지 않은 경우에만 사용하는 것을 권장합니다.

AnyStr과 같은 타입 변수와 @overload 사용 사이의 또 다른 중요한 차이점은 전자가 제네릭 클래스 타입 매개변수에 대한 제약 조건을 정의하는 데도 사용될 수 있다는 것입니다. 예를 들어, 제네릭 클래스 typing.IO의 타입 매개변수는 제약되어 있습니다 ( IO[str], IO[bytes], IO[Any]만 유효합니다).

class IO(Generic[AnyStr]): ...

스텁 파일 저장 및 배포 (Storing and distributing stub files)

스텁 파일을 저장하고 배포하는 가장 쉬운 방법은 Python 모듈과 함께 동일한 디렉토리에 두는 것입니다. 이렇게 하면 프로그래머와 도구 모두 쉽게 찾을 수 있습니다. 그러나 패키지 관리자는 패키지에 타입 힌트를 추가하지 않을 자유가 있으므로, PyPI에서 pip으로 설치할 수 있는 서드파티 스텁도 지원됩니다. 이 경우 이름 지정, 버전 관리, 설치 경로라는 세 가지 문제를 고려해야 합니다.

이 PEP는 서드파티 스텁 파일 패키지에 사용해야 하는 명명 체계에 대한 권장 사항을 제공하지 않습니다. 검색 가능성은 예를 들어 Django 패키지와 같이 패키지 인기에 기반할 것으로 기대합니다.

서드파티 스텁은 호환되는 소스 패키지의 가장 낮은 버전을 사용하여 버전이 지정되어야 합니다. 예: FooPackage는 버전 1.0, 1.1, 1.2, 1.3, 2.0, 2.1, 2.2를 가집니다. 버전 1.1, 2.0, 2.2에서 API 변경 사항이 있습니다. 스텁 파일 패키지 관리자는 모든 버전에 대한 스텁을 릴리스할 수 있지만, 최종 사용자가 모든 버전을 타입 검사할 수 있도록 최소한 1.0, 1.1, 2.0, 2.2가 필요합니다. 이는 사용자가 가장 가까운 하위 또는 동일한 버전의 스텁이 호환된다는 것을 알기 때문입니다. 제공된 예시에서 FooPackage 1.3의 경우 사용자는 스텁 버전 1.1을 선택할 것입니다.

사용자가 사용 가능한 “최신” 소스 패키지를 사용하기로 결정하면 “최신” 스텁 파일도 자주 업데이트된다면 일반적으로 작동해야 한다는 점에 유의하십시오.

서드파티 스텁 패키지는 스텁 저장소를 위해 어떤 위치든 사용할 수 있습니다. 타입 체커는 PYTHONPATH를 사용하여 이를 검색해야 합니다. 항상 확인되는 기본 대체 디렉토리는 shared/typehints/pythonX.Y/입니다 (타입 체커가 결정하는 PythonX.Y에 대해, 설치된 버전에만 국한되지 않음). 환경당 주어진 Python 버전에 대해 하나의 패키지만 설치될 수 있으므로, 해당 디렉토리 아래에서는 추가 버전 관리가 수행되지 않습니다 ( site-packages에 pip으로 bare 디렉토리를 설치하는 것과 동일). 스텁 파일 패키지 작성자는 setup.py에서 다음 스니펫을 사용할 수 있습니다.

...
data_files=[
    (
        'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
        pathlib.Path(SRC_PATH).glob('**/*.pyi'),
    ),
],
...

(업데이트: 2018년 6월 현재 서드파티 패키지에 타입 힌트를 배포하는 권장 방식이 변경되었습니다. typeshed (다음 섹션 참조) 외에도 이제 타입 힌트 배포를 위한 표준인 PEP 561이 있습니다. 이는 스텁을 포함하는 별도로 설치 가능한 패키지, 패키지의 실행 코드와 동일한 배포판에 포함된 스텁 파일, 그리고 인라인 타입 힌트를 지원하며, 후자 두 가지 옵션은 패키지에 py.typed라는 파일을 포함함으로써 가능합니다.)

Typeshed 저장소 (The Typeshed Repo)

유용한 스텁이 수집되는 공유 저장소가 있습니다. 여기에 수집된 스텁에 대한 정책은 별도로 결정되어 저장소 문서에 보고될 것입니다. 특정 패키지의 소유자가 명시적으로 제외를 요청한 경우 해당 패키지에 대한 스텁은 여기에 포함되지 않습니다.

예외 (Exceptions)

명시적으로 발생시킨 예외를 나열하기 위한 문법은 제안되지 않습니다. 현재 이 기능의 유일하게 알려진 사용 사례는 문서화이며, 이 경우 이 정보를 독스트링(docstring)에 넣는 것이 권장됩니다.

typing 모듈 (The typing Module)

Python 3.5 및 이전 버전에서도 정적 타입 검사를 사용할 수 있도록 하기 위해, 통일된 네임스페이스가 필요합니다. 이를 위해 표준 라이브러리에 typing이라는 새 모듈이 도입됩니다.

이 모듈은 타입을 구성하기 위한 기본 구성 요소(예: Any), 내장 컬렉션의 제네릭 변형을 나타내는 타입(예: List), 제네릭 컬렉션 ABC를 나타내는 타입(예: Sequence), 그리고 소수의 편의 정의를 정의합니다.

Any, Union, TypeVar를 사용하여 정의된 타입 변수와 같은 특수 타입 구문은 타입 어노테이션 컨텍스트에서만 지원되며, Generic은 베이스 클래스로만 사용될 수 있습니다. 이들 중 모두(매개변수화되지 않은 제네릭 제외)가 isinstance 또는 issubclass에 나타나면 TypeError를 발생시킵니다.

기본 구성 요소:

  • Any: def get(key: str) -> Any: ...와 같이 사용됩니다.
  • Union: Union[Type1, Type2, Type3]와 같이 사용됩니다.
  • Callable: Callable[[Arg1Type, Arg2Type], ReturnType]와 같이 사용됩니다.
  • Tuple: Tuple[int, int, str]와 같이 요소 타입을 나열하여 사용됩니다. 빈 튜플은 Tuple[()]로 타입 지정할 수 있습니다. 임의 길이의 동종 튜플은 하나의 타입과 생략 부호(ellipsis)를 사용하여 표현할 수 있습니다. 예를 들어, Tuple[int, ...]입니다. (여기서 ...는 문법의 일부인 리터럴 생략 부호입니다.)
  • TypeVar: X = TypeVar('X', Type1, Type2, Type3) 또는 단순히 Y = TypeVar('Y')와 같이 사용됩니다 (자세한 내용은 위 참조).
  • Generic: 사용자 정의 제네릭 클래스를 생성하는 데 사용됩니다.
  • Type: 클래스 객체를 어노테이션하는 데 사용됩니다.

내장 컬렉션의 제네릭 변형:

  • Dict: Dict[key_type, value_type]와 같이 사용됩니다.
  • DefaultDict: DefaultDict[key_type, value_type]와 같이 사용되며, collections.defaultdict의 제네릭 변형입니다.
  • List: List[element_type]와 같이 사용됩니다.
  • Set: Set[element_type]와 같이 사용됩니다. 아래 AbstractSet에 대한 언급 참조.
  • FrozenSet: FrozenSet[element_type]와 같이 사용됩니다.

참고: Dict, DefaultDict, List, Set, FrozenSet는 주로 반환 값을 어노테이션하는 데 유용합니다. 인자의 경우, 아래 정의된 추상 컬렉션 타입(예: Mapping, Sequence 또는 AbstractSet)을 선호합니다.

컨테이너 ABC의 제네릭 변형 (및 일부 비컨테이너):

  • Awaitable
  • AsyncIterable
  • AsyncIterator
  • ByteString
  • Callable (위 참조, 완전성을 위해 여기에 나열됨)
  • Collection
  • Container
  • ContextManager
  • Coroutine
  • Generator: Generator[yield_type, send_type, return_type]와 같이 사용됩니다. 제너레이터 함수의 반환 값을 나타냅니다. Iterable의 서브타입이며, send() 메서드가 받아들이는 타입(이 변수에서 반변성입니다. Employee 인스턴스를 보내는 것을 받아들이는 제너레이터는 Manager 인스턴스를 보내는 것을 받아들이는 제너레이터가 필요한 컨텍스트에서 유효합니다) 및 제너레이터의 반환 타입을 위한 추가 타입 변수를 가집니다.
  • Hashable (제네릭이 아니지만 완전성을 위해 존재함)
  • ItemsView
  • Iterable
  • Iterator
  • KeysView
  • Mapping
  • MappingView
  • MutableMapping
  • MutableSequence
  • MutableSet
  • Sequence
  • Set: AbstractSet으로 이름이 변경되었습니다. 이 이름 변경은 typing 모듈의 Set이 제네릭이 있는 set()를 의미하기 때문에 필요했습니다.
  • Sized (제네릭이 아니지만 완전성을 위해 존재함)
  • ValuesView

단일 특수 메서드를 테스트하는 몇 가지 일회성 타입이 정의됩니다 ( Hashable 또는 Sized와 유사).

  • Reversible: __reversed__를 테스트합니다.
  • SupportsAbs: __abs__를 테스트합니다.
  • SupportsComplex: __complex__를 테스트합니다.
  • SupportsFloat: __float__를 테스트합니다.
  • SupportsInt: __int__를 테스트합니다.
  • SupportsRound: __round__를 테스트합니다.
  • SupportsBytes: __bytes__를 테스트합니다.

편의 정의:

  • Optional: Optional[t] == Union[t, None]로 정의됩니다.
  • Text: Python 3에서는 str, Python 2에서는 unicode의 간단한 별칭입니다.
  • AnyStr: TypeVar('AnyStr', Text, bytes)로 정의됩니다.
  • NamedTuple: NamedTuple(type_name, [(field_name, field_type), ...])와 같이 사용되며 collections.namedtuple(type_name, [field_name, ...])와 동일합니다. 이는 이름 있는 튜플(named tuple) 타입의 필드 타입을 선언하는 데 유용합니다.
  • NewType: 런타임 오버헤드가 거의 없는 고유한 타입을 생성하는 데 사용됩니다. UserId = NewType('UserId', int)
  • cast(): 앞서 설명되었습니다.
  • @no_type_check: 클래스 또는 함수별로 타입 검사를 비활성화하는 데코레이터입니다 (아래 참조).
  • @no_type_check_decorator: @no_type_check와 동일한 의미를 가진 자신만의 데코레이터를 생성하는 데코레이터입니다 (아래 참조).
  • @type_check_only: 스텁 파일에서 사용하기 위해 타입 검사 중에만 사용할 수 있는 데코레이터입니다 (위 참조). 클래스 또는 함수를 런타임에 사용할 수 없음을 표시합니다.
  • @overload: 앞서 설명되었습니다.
  • get_type_hints(): 함수 또는 메서드에서 타입 힌트를 검색하는 유틸리티 함수입니다. 함수 또는 메서드 객체가 주어지면 __annotations__와 동일한 형식의 dict를 반환하지만, 원래 함수 또는 메서드 정의의 컨텍스트에서 선언되지 않은 이름 참조(문자열 리터럴로 주어짐)를 표현식으로 평가합니다.
  • TYPE_CHECKING: 런타임에는 False이지만 타입 체커에게는 True입니다.

I/O 관련 타입:

  • IO (AnyStr에 대해 제네릭)
  • BinaryIO (IO[bytes]의 간단한 서브타입)
  • TextIO (IO[str]의 간단한 서브타입)

정규 표현식 및 re 모듈 관련 타입:

  • MatchPattern: re.match()re.compile() 결과의 타입 (AnyStr에 대해 제네릭)

Python 2.7 및 양립 코드에 대한 제안된 문법 (Suggested syntax for Python 2.7 and straddling code)

일부 도구는 Python 2.7과 호환되어야 하는 코드에서 타입 어노테이션을 지원하려고 할 수 있습니다. 이를 위해 이 PEP는 함수 어노테이션이 # type: 주석에 배치되는 제안된 (그러나 필수는 아닌) 확장을 가집니다. 이러한 주석은 함수 헤더 바로 뒤(독스트링 앞)에 배치되어야 합니다. 예시: 다음 Python 3 코드:

def embezzle(self, account: str, funds: int = 1000000, *fake_receipts: str) -> None:
    """Embezzle funds from account using fake receipts."""
    <code goes here>

는 다음 코드와 동일합니다.

def embezzle(self, account, funds=1000000, *fake_receipts): # type: (str, int, *str) -> None
    """Embezzle funds from account using fake receipts."""
    <code goes here>

메서드의 경우 self에 대한 타입은 필요하지 않습니다.

인자가 없는 메서드의 경우 다음과 같습니다.

def load_cache(self): # type: () -> bool
    <code>

때로는 (아직) 인자 타입을 지정하지 않고 함수 또는 메서드의 반환 타입만 지정하고 싶을 때가 있습니다. 이를 명시적으로 지원하기 위해 인자 목록을 생략 부호(ellipsis)로 대체할 수 있습니다. 예시:

def send_email(address, sender, cc, bcc, subject, body): # type: (...) -> bool
    """Send an email message. Return True if successful."""
    <code>

때로는 매개변수 목록이 길어서 단일 # type: 주석으로 타입을 지정하기가 어려울 수 있습니다. 이를 위해 인자를 한 줄에 하나씩 나열하고, 인자 뒤에 쉼표가 있다면 각 줄마다 # type: 주석을 추가할 수 있습니다. 반환 타입을 지정하려면 생략 부호 문법을 사용합니다. 반환 타입을 지정하는 것은 필수가 아니며, 모든 인자에 타입을 지정할 필요는 없습니다. # type: 주석이 있는 줄은 정확히 하나의 인자를 포함해야 합니다. 마지막 인자(있는 경우)에 대한 타입 주석은 닫는 괄호 앞에 와야 합니다. 예시:

def send_email(address, # type: Union[str, List[str]]
               sender,  # type: str
               cc,      # type: Optional[List[str]]
               bcc,     # type: Optional[List[str]]
               subject='',
               body=None # type: List[str]
              ): # type: (...) -> bool
    """Send an email message. Return True if successful."""
    <code>

참고:

  • 이 문법을 지원하는 도구는 검사되는 Python 버전에 관계없이 이를 지원해야 합니다. 이는 Python 2와 Python 3를 모두 지원해야 하는 코드에 필수적입니다.
  • 인자 또는 반환 값이 타입 어노테이션과 타입 주석을 모두 갖는 것은 허용되지 않습니다.
  • 짧은 형식(예: # type: (str, int) -> None)을 사용할 때, 인스턴스 및 클래스 메서드의 첫 번째 인자를 제외하고 모든 인자를 고려해야 합니다(이들은 일반적으로 생략되지만 포함하는 것도 허용됩니다).
  • 짧은 형식의 경우 반환 타입은 필수입니다. Python 3에서 일부 인자나 반환 타입을 생략하는 경우, Python 2 표기법은 Any를 사용해야 합니다.
  • 짧은 형식을 사용할 때, *args**kwds의 경우 해당 타입 어노테이션 앞에 1개 또는 2개의 별표를 붙입니다. (Python 3 어노테이션과 마찬가지로, 여기의 어노테이션은 개별 인자 값의 타입을 나타내며, args 또는 kwds라는 특수 인자 값으로 받는 튜플/딕셔너리의 타입을 나타내지 않습니다.)
  • 다른 타입 주석과 마찬가지로, 어노테이션에 사용된 모든 이름은 어노테이션을 포함하는 모듈에 의해 임포트되거나 정의되어야 합니다.
  • 짧은 형식을 사용할 때, 전체 어노테이션은 한 줄이어야 합니다. 짧은 형식은 닫는 괄호와 같은 줄에 올 수도 있습니다. 예를 들면:

    def add(a, b): # type: (int, int) -> int
        return a + b
    
  • 잘못 배치된 타입 주석은 타입 체커에 의해 오류로 표시됩니다. 필요한 경우, 이러한 주석은 두 번 주석 처리될 수 있습니다. 예를 들면:

    def f():
        '''Docstring'''
        # type: () -> None # Error!
    
    def g():
        '''Docstring'''
        # # type: () -> None # This is OK
    
  • Python 2.7 코드를 검사할 때, 타입 체커는 intlong 타입을 동일하게 처리해야 합니다. Text로 타입 지정된 매개변수의 경우, str 타입과 unicode 타입의 인자 모두 허용되어야 합니다.

거부된 대안 (Rejected Alternatives)

이 PEP의 초기 초안을 논의하는 동안 다양한 반대 의견이 제기되었고 대안이 제안되었습니다. 여기서는 그 중 일부를 논의하고 왜 거부되었는지 설명합니다.

몇 가지 주요 반대 의견이 제기되었습니다.

제네릭 타입 매개변수에는 어떤 괄호를 사용할 것인가? (Which brackets for generic type parameters?)

대부분의 사람들은 C++, Java, C#, Swift와 같은 언어에서 제네릭 타입의 매개변수화를 표현하기 위해 꺾쇠괄호(예: List<int>)를 사용하는 것에 익숙합니다. 이러한 괄호의 문제는 특히 Python과 같은 단순한 파서에게는 구문 분석(parse)하기가 정말 어렵다는 것입니다. 대부분의 언어에서 모호성은 일반적으로 일반 표현식이 허용되지 않는 특정 구문 위치에서만 꺾쇠괄호를 허용함으로써 처리됩니다. (또한 임의의 코드 섹션을 되돌아갈 수 있는 매우 강력한 파싱 기술을 사용함으로써).

그러나 Python에서는 타입 표현식이 (구문적으로) 다른 표현식과 동일하기를 원하므로, 예를 들어 변수 할당을 사용하여 타입 별칭을 생성할 수 있습니다. 이 간단한 타입 표현식을 고려해 봅시다.

List<int>

Python 파서의 관점에서, 이 표현식은 연결된 비교(chained comparison)와 동일한 네 개의 토큰(NAME, LESS, NAME, GREATER)으로 시작합니다.

a < b > c # I.e., (a < b) and (b > c)

두 가지 방식으로 구문 분석될 수 있는 예시를 만들 수도 있습니다.

a < b > [ c ]

언어에 꺾쇠괄호가 있다고 가정하면, 이것은 다음 두 가지 중 하나로 해석될 수 있습니다.

(a<b>)[c] # I.e., (a<b>).__getitem__(c)
a < b > ([c]) # I.e., (a < b) and (b > [c])

이러한 경우를 명확히 하는 규칙을 고안하는 것이 확실히 가능하겠지만, 대부분의 사용자에게는 규칙이 자의적이고 복잡하게 느껴질 것입니다. 또한 CPython 파서(및 Python의 모든 다른 파서)를 크게 변경해야 할 것입니다. Python의 현재 파서는 의도적으로 “단순”하다는 점에 유의해야 합니다. 간단한 문법은 사용자가 추론하기 더 쉽습니다.

이러한 모든 이유로 인해 대괄호(예: List[int])는 제네릭 타입 매개변수에 선호되는 (오랫동안 그래왔던) 문법입니다. 이들은 메타클래스에 __getitem__() 메서드를 정의하여 구현될 수 있으며, 새로운 문법은 전혀 필요하지 않습니다. 이 옵션은 Python의 모든 최신 버전(Python 2.2부터 시작)에서 작동합니다. Python은 이러한 구문 선택에서 혼자가 아닙니다. Scala의 제네릭 클래스도 대괄호를 사용합니다.

기존 어노테이션 사용은 어떻게 되는가? (What about existing uses of annotations?)

한 가지 주장은 PEP 3107이 함수 어노테이션에 임의의 표현식 사용을 명시적으로 지원한다고 지적합니다. 새로운 제안은 PEP 3107의 사양과 호환되지 않는 것으로 간주됩니다.

이에 대한 우리의 답변은, 우선 현재 제안이 어떠한 직접적인 비호환성도 도입하지 않으므로, Python 3.4에서 어노테이션을 사용하는 프로그램은 Python 3.5에서도 올바르고 편견 없이 계속 작동할 것이라는 점입니다.

우리는 타입 힌트가 결국 어노테이션의 유일한 사용처가 되기를 바라지만, 이를 위해서는 Python 3.5에 typing 모듈이 처음 출시된 후 추가 논의와 폐기 기간이 필요할 것입니다. 현재 PEP는 Python 3.6이 출시될 때까지 임시 상태(PEP 411 참조)를 가질 것입니다. 가장 빠른 계획은 3.6에서 비타입 힌트 어노테이션의 조용한 폐기, 3.7에서 완전한 폐기, 그리고 Python 3.8에서 타입 힌트를 어노테이션의 유일한 허용된 사용으로 선언하는 것입니다. 이것은 어노테이션을 사용하는 패키지 작성자들에게 타입 힌트가 즉시 성공하더라도 다른 접근 방식을 고안할 충분한 시간을 줄 것입니다.

(업데이트: 2017년 가을 현재, 이 PEP와 typing.py 모듈의 임시 상태 종료 시점 및 다른 어노테이션 사용에 대한 폐기 일정이 변경되었습니다. 업데이트된 일정은 PEP 563을 참조하십시오.)

또 다른 가능한 결과는 타입 힌트가 결국 어노테이션의 기본 의미가 되지만, 항상 이를 비활성화할 수 있는 옵션이 남아있을 것이라는 점입니다. 이를 위해 현재 제안은 주어진 클래스 또는 함수에서 어노테이션의 기본 해석을 타입 힌트로 비활성화하는 데코레이터 @no_type_check를 정의합니다. 또한 데코레이터를 데코레이터(!)로 만드는 메타 데코레이터 @no_type_check_decorator를 정의하여, 후자로 데코레이터된 모든 함수 또는 클래스의 어노테이션이 타입 체커에 의해 무시되도록 합니다.

# type: ignore 주석도 있으며, 정적 체커는 선택된 패키지에서 타입 검사를 비활성화하는 구성 옵션을 지원해야 합니다.

이러한 모든 옵션에도 불구하고, 개별 인자에 대해 타입 힌트와 다른 형태의 어노테이션을 공존하게 허용하자는 제안이 유포되었습니다. 한 제안은 주어진 인자에 대한 어노테이션이 딕셔너리 리터럴인 경우, 각 키는 다른 형태의 어노테이션을 나타내고, type 키는 타입 힌트에 사용될 것이라고 제안합니다. 이 아이디어와 그 변형의 문제는 표기법이 매우 “시끄럽고” 읽기 어렵다는 것입니다. 또한 기존 라이브러리가 어노테이션을 사용하는 대부분의 경우, 이를 타입 힌트와 결합할 필요가 거의 없을 것입니다. 따라서 타입 힌트를 선택적으로 비활성화하는 더 간단한 접근 방식이 충분해 보입니다.

선언되지 않은 이름 참조의 문제 (The problem of forward declarations)

타입 힌트가 선언되지 않은 이름 참조를 포함해야 하는 경우 현재 제안은 확실히 최적화되어 있지 않습니다. Python은 모든 이름이 사용될 때까지 정의되어야 한다고 요구합니다. 순환 임포트(circular imports)를 제외하고는 거의 문제가 되지 않습니다. 여기서 “사용”은 “런타임에 조회”를 의미하며, 대부분의 “선언되지 않은 이름 참조”에서는 함수가 호출되기 전에 이름이 정의되었는지 확인하는 데 문제가 없습니다.

타입 힌트의 문제는 어노테이션이 (PEP 3107에 따라, 기본값과 유사하게) 함수가 정의될 때 평가되므로, 어노테이션에 사용된 모든 이름은 함수가 정의될 때 이미 정의되어 있어야 한다는 것입니다. 일반적인 시나리오는 메서드에서 클래스 자체를 어노테이션에서 참조해야 하는 클래스 정의입니다. (더 일반적으로, 상호 재귀 클래스에서도 발생할 수 있습니다.) 이는 컨테이너 타입에 자연스럽습니다. 예를 들면:

class Node:
    """Binary tree node."""
    def __init__(self, left: Node, right: Node):
        self.left = left
        self.right = right

이렇게 작성하면 작동하지 않을 것입니다. Python의 특이성 때문에 클래스 이름은 클래스 본문 전체가 실행된 후에 정의되기 때문입니다. 우리의 해결책은 특별히 우아하지는 않지만, 작업을 수행하는 것으로, 어노테이션에 문자열 리터럴을 사용하는 것을 허용하는 것입니다. 그러나 대부분의 경우 이를 사용할 필요는 없을 것입니다. 대부분의 타입 힌트 사용은 내장 타입 또는 다른 모듈에 정의된 타입을 참조할 것으로 예상됩니다.

대안 제안은 타입 힌트의 의미를 변경하여 런타임에 전혀 평가되지 않도록 하는 것입니다 (결국 타입 검사는 오프라인에서 이루어지므로, 타입 힌트가 런타임에 전혀 평가될 필요가 있을까요?). 물론 이는 하위 호환성(backwards compatibility)을 침해할 것입니다. Python 인터프리터는 특정 어노테이션이 타입 힌트를 의미하는지 아니면 다른 것을 의미하는지 실제로 알지 못하기 때문입니다.

다음과 같이 __future__ 임포트를 통해 주어진 모듈의 모든 어노테이션을 문자열 리터럴로 변환할 수 있는 절충안이 가능합니다.

from __future__ import annotations

class ImSet:
    def add(self, a: ImSet) -> List[ImSet]: ...

assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}

이러한 __future__ 임포트 문은 별도의 PEP에서 제안될 수 있습니다.

(업데이트: 해당 __future__ 임포트 문과 그 결과는 PEP 563에서 논의됩니다.)

이중 콜론 (The double colon)

몇몇 창의적인 사람들은 이 문제에 대한 해결책을 고안하려고 노력했습니다. 예를 들어, 타입 힌트에 이중 콜론(::)을 사용하여 타입 힌트와 다른 어노테이션을 구별하고, 런타임 평가를 배제하도록 의미를 변경하는 것이 제안되었습니다. 그러나 이 아이디어에는 여러 가지 문제가 있습니다.

  • 못생겼습니다. Python의 단일 콜론은 많은 용도로 사용되며, 그 모든 용도는 영어 텍스트에서 콜론의 사용과 유사하여 친숙하게 느껴집니다. 이는 Python이 대부분의 구두점 형식에 대해 따르는 일반적인 경험칙입니다. 예외는 일반적으로 다른 프로그래밍 언어에서 잘 알려져 있습니다. 그러나 ::의 이러한 사용은 영어에서는 들어본 적이 없으며, 다른 언어(예: C++)에서는 스코핑 연산자로 사용되는데, 이는 매우 다른 종류의 것입니다. 대조적으로, 타입 힌트를 위한 단일 콜론은 자연스럽게 읽힙니다. 이는 신중하게 이 목적을 위해 설계되었기 때문입니다(이 아이디어는 PEP 3107보다 훨씬 오래되었습니다). 또한 Pascal에서 Swift에 이르는 다른 언어에서도 동일한 방식으로 사용됩니다.
  • 반환 타입 어노테이션에는 어떻게 할 것인가?
  • 타입 힌트가 런타임에 평가되는 것은 실제로 기능입니다. 런타임에 타입 힌트를 사용할 수 있게 하면 런타임 타입 체커를 타입 힌트 위에 구축할 수 있습니다. 타입 체커가 실행되지 않을 때도 실수를 잡아냅니다. 별도의 프로그램이므로 사용자는 이를 실행하지 않거나(심지어 설치하지 않을 수도 있지만), 여전히 타입 힌트를 간결한 문서 형식으로 사용하고 싶어 할 수 있습니다. 깨진 타입 힌트는 문서로도 사용할 수 없습니다.
  • 새로운 문법이기 때문에 타입 힌트에 이중 콜론을 사용하면 Python 3.5에서만 작동하는 코드로 제한될 것입니다. 기존 문법을 사용함으로써 현재 제안은 이전 Python 3 버전에서도 쉽게 작동할 수 있습니다. (그리고 실제로 mypy는 Python 3.2 이상을 지원합니다.) 타입 힌트가 성공적이라면, 예를 들어 var age: int = 42와 같이 변수의 타입을 선언하기 위한 새로운 문법을 미래에 추가하기로 결정할 수도 있습니다. 인자 타입 힌트에 이중 콜론을 사용했다면, 일관성을 위해 미래 문법에도 동일한 규칙을 사용해야 하므로 추악함이 영속될 것입니다.

다른 형태의 새로운 문법 (Other forms of new syntax)

where 키워드의 도입, Cobra에서 영감을 받은 requires 절과 같은 몇 가지 다른 형태의 대체 문법이 제안되었습니다. 그러나 이들은 모두 이중 콜론과 공통적인 문제를 공유합니다. 즉, 이전 Python 3 버전에서는 작동하지 않을 것입니다. 새로운 __future__ 임포트에도 동일하게 적용될 것입니다.

다른 하위 호환 가능한 규칙 (Other backwards compatible conventions)

제시된 아이디어에는 다음이 포함됩니다.

  • 데코레이터, 예를 들어 @typehints(name=str, returns=str). 이것은 작동할 수 있지만, 매우 장황하며(추가 한 줄, 인자 이름 반복), PEP 3107 표기법의 우아함과는 거리가 니다.
  • 스텁 파일. 스텁 파일은 원하지만, 이는 주로 서드파티 패키지, Python 2와 Python 3를 모두 지원해야 하는 코드, 특히 확장 모듈과 같이 타입 힌트를 추가하기 어려운 기존 코드에 타입 힌트를 추가하는 데 유용합니다. 대부분의 상황에서 함수 정의와 함께 어노테이션을 인라인으로 두는 것이 훨씬 더 유용합니다.
  • 독스트링(Docstrings). Sphinx 표기법( :type arg1: description )을 기반으로 하는 기존 독스트링 규칙이 있습니다. 이것은 매우 장황하며(매개변수당 추가 한 줄), 그다지 우아하지 않습니다. 새로운 것을 만들 수도 있지만, 어노테이션 문법을 이기기는 어렵습니다(바로 이 목적을 위해 설계되었기 때문입니다).

다른 릴리스를 기다리자는 제안도 있었습니다. 그러나 그것이 어떤 문제를 해결할까요? 단지 미루는 것일 뿐입니다.

PEP 개발 과정 (PEP Development Process)

이 PEP의 라이브 초안은 GitHub에 있습니다. 또한 기술적인 논의의 대부분이 이루어지는 이슈 트래커도 있습니다.

GitHub의 초안은 작은 단위로 정기적으로 업데이트됩니다. 공식 PEP 저장소는 (일반적으로) 새로운 초안이 python-dev에 게시될 때만 업데이트됩니다.

감사 (Acknowledgements)

이 문서는 Jim Baker, Jeremy Siek, Michael Matson Vitousek, Andrey Vlasovskikh, Radomir Dopieralski, Peter Ludemann, 그리고 BDFL-Delegate인 Mark Shannon의 귀중한 의견, 격려 및 조언 없이는 완성될 수 없었습니다.

영향을 준 것에는 PEP 482에 언급된 기존 언어, 라이브러리 및 프레임워크가 포함됩니다. 알파벳순으로 그들의 창작자들에게 감사드립니다: Stefan Behnel, William Edwards, Greg Ewing, Larry Hastings, Anders Hejlsberg, Alok Menghrajani, Travis E. Oliphant, Joe Pamer, Raoul-Gabriel Urma, Julien Verlaguet.

이 문서는 공공 도메인에 배치되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments