Skip to content

Python:typing

This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable, TypeVar, and Generic. For a full specification, please see PEP 484. For a simplified introduction to type hints, see PEP 483.

Annotated

Add context-specific metadata to a type.

Example: Annotated[int, runtime_check.Unsigned] indicates to the hypothetical runtime_check module that this type is an unsigned int. Every other consumer of this type can ignore this metadata and treat this type as int.

The first argument to Annotated must be a valid type.

Details:

  • It's an error to call Annotated with less than two arguments.
  • Access the metadata via the __metadata__ attribute::
assert Annotated[int, '$'].__metadata__ == ('$',)
  • Nested Annotated types are flattened::
assert Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
  • Instantiating an annotated type is equivalent to instantiating the underlying type::
assert Annotated[C, Ann1](5) == C(5)
  • Annotated can be used as a generic type alias::

Optimized:

TypeAlias = Annotated[T, runtime.Optimize()]
assert Optimized[int] == Annotated[int, runtime.Optimize()]

OptimizedList:

TypeAlias = Annotated[list[T], runtime.Optimize()]
assert OptimizedList[int] == Annotated[list[int], runtime.Optimize()]
  • Annotated cannot be used with an unpacked TypeVarTuple::

Variadic:

TypeAlias = Annotated[*Ts, Ann1]  # NOT valid

This would be equivalent to::

Annotated[T1, T2, T3, ..., Ann1]

where T1, T2 etc. are TypeVars, which would be invalid, because only one type should be passed to Annotated.

At Runtime

def annotated_by(
    annotated: object,
    kind: type[T],
) -> Iterable[tuple[str, T, type]]:
    for k, v in get_type_hints(annotated, include_extras=True).items():
        all_args = get_args(v)
        if not all_args:
            continue
        actual, *rest = all_args
        for arg in rest:
            if isinstance(arg, kind):
                yield k, arg, actual

이렇게 하면 사용하기 쉬운 편리한 형식으로 우리가 찾고 있는 주석을 다시 얻을 수 있습니다. 다음은 이를 사용하는 방법에 대한 간단한 예입니다.

@dataclass
class AnAnnotation:
    name: str

def a_function(
    a: str,
    b: Annotated[int, AnAnnotation("b")],
    c: Annotated[float, AnAnnotation("c")],
) -> None:
    ...

print(list(annotated_by(a_function, AnAnnotation)))

# [('b', AnAnnotation(name='b'), <class 'int'>),
#  ('c', AnAnnotation(name='c'), <class 'float'>)]

Callable

Frameworks expecting callback functions of specific signatures might be type hinted using:

Callable[[Arg1Type, Arg2Type], ReturnType]

For example:

from collections.abc 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

It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis for the list of arguments in the type hint:

Callable[..., ReturnType]

참고로 Callable[[...], ReturnType] 는 허용되지 않는 문법이다.

ParamSpec

#Callable 타입의 콜백 함수들을 인자로 가지는 경우에는 해당 콜백 함수의 인자의 타입에 이를 호출하는 함수가 의존하는 경우가 있습니다.

데코레이터가 대표적인데요. 이럴 때는 ParamSpec을 사용해주실 수 있습니다. (이 역시 Generic 타입의 일종으로 볼 수 있습니다.)

from typing import TypeVar, Callable, ParamSpec
import logging


T = TypeVar('T')
P = ParamSpec('P')


def add_logging(f: Callable[P, T]) -> Callable[P, T]:
    '''A type-safe decorator to add logging to a function.'''
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        logging.info(f'{f.__name__} was called')
        return f(*args, **kwargs)
    return inner


@add_logging
def add_two(x: float, y: float) -> float:
    '''Add two numbers together.'''
    return x + y


@add_logging
def send_msg(msg: str) -> None:
    print(f"I Sent {msg}")

ModuleType

typing 이 아닌, types에 있다.

import types
types.ModuleType

아니면 직접 추가하자.

import sys
ModuleType = type(sys)

Awaitable Callable

비동기 함수는 다음과 같이 사용하면 된다.

from typing import Any, Union, Callable, Awaitable

Watcher = Callable[..., Union[Awaitable[Any], Any]]

# ...

from inspect import iscoroutinefunction

if iscoroutinefunction(watcher):
    return await watcher(*args, **kwargs)  # type: ignore[misc]
else:
    return watcher(*args, **kwargs)

Generics

List[X]와 같은 제네릭 클래스를 사용하는 방법:

from typing import TypeVar, Generic

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        # Create an empty list with items of type T
        self.items: List[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self) -> T:
        return self.items.pop()

    def empty(self) -> bool:
        return not self.items

다음과 같이 사용할 수 있다:

# Construct an empty Stack[int] instance
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x')        # Type error

유형 추론은 사용자 정의 제네릭 유형에서도 작동합니다:

def process(stack: Stack[int]) -> None: ...

process(Stack())   # Argument has inferred type Stack[int]

제네릭 유형의 인스턴스 생성도 유형 검사됩니다.

class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

Box(1)  # OK, inferred type is Box[int]
Box[int](1)  # Also OK
s = 'some string'
Box[int](s)  # Type error

init_subclass

class_getitem

Parameterizes a generic class.

At least, parameterizing a generic class is the *main* thing this method
does. For example, for some generic class `Foo`, this is called when we
do `Foo[int]` - there, with `cls=Foo` and `params=int`.

However, note that this method is also called when defining generic
classes in the first place with `class Foo(Generic[T]): ...`.

제네릭 클래스에서 MyClass[int]와 같은 타입 인덱싱을 지원하기 위해 사용됩니다.

TypeVar

특정 타입 중 하나만 허용 (정확히 일치해야 한다):

from typing import TypeVar

T = TypeVar("T", int, str, bytes)

bound=... 를 사용하면 해당 타입 또는 상속된 타입 허용:

from typing import TypeVar, Union

T = TypeVar("T", bound=Union[int, str, bytes])

공변성 (Covariant)

타입 분산(variance)에 대한 내용이다.

Python 타입 지정에서는 제네릭 클래스의 타입 매개변수가 하위 타입 관계에서 어떻게 동작할지 지정할 수 있습니다. 타입 분산의 세 가지 유형은 다음과 같습니다:

  • 공변 (+T): 주어진 타입의 하위 타입도 허용합니다. covariant=True 플래그를 사용하면 된다.
  • 반변적 (-T): 주어진 타입의 상위 타입도 허용합니다. contravariant=True 플래그를 사용하면 된다.
  • 불변: 하위 타입이나 상위 타입을 허용하지 않으며 정확히 동일한 타입만 허용합니다.

Example:

from typing import TypeVar, Generic

# T is covariant
T = TypeVar('T', covariant=True)

# Base class
class Animal:
    pass

# Subclass
class Dog(Animal):
    pass

# A generic class with a covariant type parameter
class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

# Because T is covariant, Box[Dog] can be assigned to Box[Animal]
dog_box: Box[Dog] = Box(Dog())
animal_box: Box[Animal] = dog_box

print(isinstance(animal_box.content, Animal))  # 출력: True

위 예제에서 Tcovariant=True로 선언했기 때문에, Box[Dog]Box[Animal]에 할당될 수 있습니다.

공변 타입 변수 덕분에 자식 클래스(Dog)가 부모 클래스(Animal)로 올바르게 취급됩니다.

Type aliases

A type alias is defined by assigning the type to the alias. In this example, Vector and List[float] will be treated as interchangeable synonyms:

from typing import List
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

# typechecks; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

Type aliases are useful for simplifying complex type signatures. For example:

from typing import Dict, Tuple, Sequence

ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
    ...

# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
        message: str,
        servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
    ...

Note that None as a type hint is a special case and is replaced by type(None).

NewType

Rust에서는 일반적인 데이터 타입 (e.g. 정수)에 새로운 동작을 추가하지 않고, 도메인에 걸맞는 용도로 새롭게 이름을 지정하는 데이터 타입을 정의하는게 일반적입니다. 이 패턴을 “newtype”이라고 하며 Python에서도 사용할 수 있습니다.

아래의 상황을 한번 봅시다:

class Database:
  def get_car_id(self, brand: str) -> int:
  def get_driver_id(self, name: str) -> int:
  def get_ride_info(self, car_id: int, driver_id: int) -> RideInfo:

db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
info = db.get_ride_info(driver_id, car_id)

get_ride_info의 인수가 잘못들어갔습니다. </code>db.get_rider_info(car_id, driver_id)</code>가 되어야합니다. 하지만, 차량 ID와 운전자 ID는 모두 정수에 불과하므로 의미상 함수 호출이 잘못되었더라도 타입 자체는 올바르므로 타입 오류는 없습니다.

이 문제는 "NewType"을 사용하여 서로 다른 종류의 ID에 대해 별도의 타입을 정의함으로써 해결할 수 있습니다:

from typing import NewType

# Define a new type called "CarId", which is internally an `int`
CarId = NewType("CarId", int)
# Ditto for "DriverId"
DriverId = NewType("DriverId", int)

class Database:
  def get_car_id(self, brand: str) -> CarId:
  def get_driver_id(self, name: str) -> DriverId:
  def get_ride_info(self, car_id: CarId, driver_id: DriverId) -> RideInfo:


db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
# Type error here -> DriverId used instead of CarId and vice-versa
info = db.get_ride_info(driver_id, car_id)  ## 경고 포착!

ForwardRef

문자열 정방향 참조의 내부 입력 표현에 사용되는 클래스입니다. 예를 들어 List["SomeClass"]는 암시 적으로 List[ForwardRef("SomeClass")]. 이 클래스는 사용자가 인스턴스화해서는 안되지만 내부 검사 도구에서 사용할 수 있습니다.

Protocol

PEP 544 항목 참조.

TypeGuard

Type Narrowing (타입 좁히기) 을 위해 필요.

TypeGuard는 Python의 정적 타입 검사기(mypy)에서 사용되는 유용한 도구로, 조건에 따라 특정 타입을 확인하고, 그 타입이 참인 경우 해당 타입으로 변수를 추론할 수 있도록 돕습니다. Python 3.10에서 도입된 PEP 647에 정의된 개념입니다.

일반적으로 타입 추론은 함수의 반환 값이나 변수가 고정된 타입을 가지는 경우 유용하지만, 조건문 등을 통해 타입을 좁히는 경우 그 타입을 정확히 추론하기 어려울 수 있습니다. TypeGuard를 사용하면 이러한 상황에서 타입 힌트를 추가로 제공하여 더 나은 타입 추론을 가능하게 만듭니다.

def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
    '''Determines whether all objects in the list are strings'''
    return all(isinstance(x, str) for x in val)

def func1(val: list[object]):
    if is_str_list(val):
        # Type of ``val`` is narrowed to ``list[str]``.
        print(" ".join(val))
    else:
        # Type of ``val`` remains as ``list[object]``.
        print("Not a list of strings!")
  • TypeGuard 정의: 함수의 반환 타입으로 TypeGuard를 지정하면, 해당 함수가 True를 반환할 경우 인수의 타입을 지정된 타입으로 좁힐 수 있습니다.
  • 조건에 따른 타입 좁히기: 위 예시에서 is_str_list 함수는 리스트의 모든 요소가 문자열인 경우 True를 반환하며, 그 결과로 타입 검사기는 리스트를 list[str]로 추론합니다.

Unpack

INFORMATION

Added in version 3.11

from typing import TypedDict, Unpack

class Movie(TypedDict):
    name: str
    year: int

# This function expects two keyword arguments - `name` of type `str`
# and `year` of type `int`.
def foo(**kwargs: Unpack[Movie]):
    kwargs["name"] = ... # 여기에서 "name" 을 칠 때 AutoComplete 나타난다. @dataclass 클래스는 안되더라

See PEP 692 for more details on using Unpack for **kwargs typing.

overload

인자에 따라 반환값이 달라지거나 등, IDE의 타입 추론에 도움이 된다. 사용사례는 다음과 같다:

from os import environ
from typing import Dict, Optional, TypeVar, Union, overload

from your_api.types.string.to_boolean import string_to_boolean

DefaultT = TypeVar("DefaultT", str, bool, int, float)


@overload
def get_typed_environ_value(key: str) -> Optional[str]: ...


@overload
def get_typed_environ_value(key: str, default: str) -> str: ...


@overload
def get_typed_environ_value(key: str, default: bool) -> bool: ...


@overload
def get_typed_environ_value(key: str, default: int) -> int: ...


@overload
def get_typed_environ_value(key: str, default: float) -> float: ...


def get_typed_environ_value(
    key: str,
    default: Optional[DefaultT] = None,
) -> Optional[Union[str, bool, int, float]]:
    if default is None:
        return environ.get(key)

    value = environ.get(key, str(default))
    if isinstance(default, str):
        return value
    elif isinstance(default, bool):
        return string_to_boolean(value)
    elif isinstance(default, int):
        return int(value)
    elif isinstance(default, float):
        return float(value)
    else:
        raise TypeError(f"Unsupported default type: {type(default).__name__}")

get_args

타입 안에 들어간 인자들을 받아올 수 있다. (개인적으로) 리터럴 인자 목록을 가져올 때 사용한다.

TimedRotatingWhenLiteral = Literal[
    "S", "M", "H", "D", "W0", "W1", "W2", "W3", "W4", "W5", "W6", "midnight"
]  # W0=Monday

TIMED_ROTATING_WHEN: Final[Sequence[str]] = get_args(TimedRotatingWhenLiteral)

def my_func(when: str):
    assert when in TIMED_ROTATING_WHEN

Union 의 클래스 목록을 확인할 수도 있다.

from enum import IntEnum
from typing import Union, get_args

class Test1(IntEnum):
    A = 1
    B = 2

class Test2(IntEnum):
    C = 3
    D = 4

AnyTest = Union[Test1, Test2)
print(get_args(AnyTest))  # -> (<enum 'Test1'>, <enum 'Test2'>)

ABC vs Prototype

요약하면, 이미 상속 구조가 잡혀있어서 컨트롤 하기 힘들다면 Prototype, 처음부터 설계 가능하다면 ABC

상속 클래스 타입 확인

class User: ...
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

# Accepts User, BasicUser, ProUser, TeamUser, ...
def make_new_user(user_class: Type[User]) -> User:
    # ...
    return user_class()

함수의 파라미터 타입 확인

import typing

def f(x: typing.List[MyType]):
    ...

print(f.__annotations__["x"].__args__[0])  # prints <class '__main__.MyType'>
print(f.__annotations__["x"].__base__)  # prints typing.List[float]
print(f.__annotations__["x"].__orig_bases__[0])  # prints <class 'list'>

get_type_hints를 사용하는 방법이 더 좋다.

from typing import get_type_hints

class Person:
    name: str
    age: int

get_type_hints(Person)
# returns {'name': <class 'str'>, 'age': <class 'int'>}

최신 갱신

inspect 모듈의 signature 함수를 사용하는걸 추천한다.

Troubleshooting

Invariant type variable

cvp/patterns/proxies/callable_proxy.py:10:1: error: Invariant type variable
"ValueT" used in protocol where covariant one is expected  [misc]
    class ReadableCallableProtocol(Protocol[ValueT]):

직역하면

"ValueT"가 공변성(Covariant)가 예상되지만 불변형(Invariant) 으로 사용됨

공변(covariant)이 필요한 상황에서 불변(invariant)으로 사용되었기 때문에 발생합니다. 자세한 내용은 #공변성 (Covariant) 항목 참조.

간단히, TypeVar(..., covariant=True)를 사용하면 된다.

error: Type variable "..." is unbound [valid-type]

mypy에서 다음과 같이 출력됨:

cvp/wsdl/schema.py: note: In class "XsdSchema":
cvp/wsdl/schema.py:21:19: error: Type variable "cvp.wsdl.schema._UrlKey" is unbound  [valid-type]
        _loaded: Dict[_UrlKey, _EtreeElement]
                      ^
cvp/wsdl/schema.py:21:19: note: (Hint: Use "Generic[_UrlKey]" or "Protocol[_UrlKey]" base class to bind "_UrlKey" inside a class)
cvp/wsdl/schema.py:21:19: note: (Hint: Use "_UrlKey" in function signature to bind "_UrlKey" inside a function)

이 때 에러가 발생된 _UrlKey 를 보면 다음과 같다:

_UrlKey = str

이걸 다음과 같이 수정:

from typing import TypeAlias

_UrlKey: TypeAlias = str

See also

Favorite site