annotationlib — 애너테이션 조사 기능

annotationlib — 애너테이션 조사 기능 (Functionality for introspecting annotations)

annotationlib 모듈은 모듈·클래스·함수에 달린 **애너테이션(annotation)**을 들여다보는 도구를 제공해요. 애너테이션은 느리게(lazily) 평가되고 애너테이션이 만들어질 때 아직 정의되지 않은 객체에 대한 **전방 참조(forward reference)**를 자주 담고 있어요. 이 모듈은 그런 전방 참조와 기타 경계 상황에서도 애너테이션을 안정적으로 가져올 수 있는 저수준 도구들을 제공해요.

출처: Python 표준 라이브러리

본문

annotationlib 모듈은 모듈, 클래스, 함수에 달린 애너테이션을 들여다보는(introspect) 도구를 제공해요. 3.14 버전에서 추가됐어요. (소스 코드: Lib/annotationlib.py)

애너테이션은 느리게 평가되고, 애너테이션이 만들어질 때 아직 정의되지 않은 객체에 대한 전방 참조를 자주 포함해요. 이 모듈은 전방 참조와 다른 경계 상황이 있어도 애너테이션을 안정적으로 가져올 수 있는 저수준 도구 세트를 제공해요.

이 모듈은 애너테이션을 **세 가지 주요 형식(Format)**으로 가져오는 걸 지원해요. 각각 서로 다른 사용 사례에 잘 맞죠.

  • VALUE — 애너테이션을 평가해서 그 값을 반환해요. 다루기 가장 직관적이지만, 예를 들어 애너테이션이 정의되지 않은 이름을 참조하면 오류를 발생시킬 수 있어요.
  • FORWARDREF — 해석할 수 없는 애너테이션에 대해 ForwardRef 객체를 반환해요. 평가하지 않고 애너테이션을 들여다볼 수 있게 해 주죠. 해결되지 않은 전방 참조가 있을 수 있는 애너테이션을 다룰 때 유용해요.
  • STRING — 애너테이션을 문자열로 반환해요. 소스 파일에 나타나는 모습과 비슷하죠. 애너테이션을 읽기 좋게 표시하고 싶은 문서 생성기 등에 유용해요.

get_annotations() 함수가 애너테이션을 가져오는 주 진입점이에요. 함수, 클래스, 모듈을 주면 요청한 형식의 애너테이션 사전을 반환해요. 이 모듈은 또한 애너테이션을 평가하는 데 쓰는 annotate 함수를 직접 다루는 기능(예: get_annotate_from_class_namespace(), call_annotate_function())과, evaluate 함수를 다루는 call_evaluate_function()을 제공해요.

주의: 이 모듈의 대부분 기능은 임의의 코드를 실행할 수 있어요. 보안 섹션을 꼭 확인하세요.

더 알아보기

  • PEP 649 — Python에서 애너테이션이 작동하는 현재 모델을 제안했어요.
  • PEP 749 — PEP 649의 여러 측면을 확장하고 annotationlib 모듈을 소개했어요.
  • Annotations Best Practices — 애너테이션을 다루는 모범 사례.
  • typing-extensions — 이전 Python 버전에서 동작하는 get_annotations()의 백포트를 제공해요.

애너테이션 의미론 (Annotation semantics)

애너테이션 평가 방식은 Python 3의 역사를 거치며 바뀌었고, 현재도 future import에 의존해요. 지금까지 세 가지 실행 모델이 있었어요.

  • Stock 의미론(Python 3.0~3.13의 기본값; PEP 3107, PEP 526 참고) — 애너테이션을 소스 코드에서 만나는 즉시 적극적으로(eagerly) 평가해요.
  • 문자열화된 애너테이션(Python 3.7 이상에서 from __future__ import annotations와 함께 사용; PEP 563 참고) — 애너테이션을 문자열로만 저장해요.
  • 지연 평가(Deferred evaluation)(Python 3.14 이상의 기본값; PEP 649, PEP 749 참고) — 애너테이션은 접근할 때만 느리게 평가돼요.

다음 프로그램을 예로 들어 볼게요.

def func(a: Cls) -> None:
    print(a)

class Cls: pass

print(func.__annotations__)

이건 이렇게 동작해요.

  • stock 의미론(Python 3.13 이하): func가 정의되는 줄에서 NameError를 던져요. 그 시점에 Cls는 정의되지 않은 이름이니까요.
  • 문자열화된 애너테이션(from __future__ import annotations 사용 시): {'a': 'Cls', 'return': 'None'}을 출력해요.
  • 지연 평가(Python 3.14 이상): {'a': <class 'Cls'>, 'return': None}을 출력해요.

함수 애너테이션이 Python 3.0(PEP 3107)에서 처음 도입됐을 때는 구현이 가장 단순하고 명확한 방식이라 stock 의미론이 쓰였어요. 변수 애너테이션이 Python 3.6(PEP 526)에 도입될 때도 같은 실행 모델이 사용됐죠. 하지만 애너테이션을 타입 힌트로 쓸 때 문제가 생겼어요. 애너테이션을 만날 때 아직 정의되지 않은 이름을 참조할 필요가 생기는 경우가 있거든요. 게다가 모듈 import 시점에 애너테이션을 실행하는 건 성능 문제도 있었어요. 그래서 Python 3.7에서 PEP 563이 from __future__ import annotations 문법으로 애너테이션을 문자열로 저장하는 기능을 소개했죠. 당시 계획은 언젠가 이 동작을 기본값으로 만들려는 거였는데, 문제가 생겼어요. 런타임에 애너테이션을 들여다보는 사람들에게 문자열화된 애너테이션은 처리하기 더 어렵거든요. 대안 제안인 PEP 649가 세 번째 실행 모델인 지연 평가를 소개했고 Python 3.14에서 구현됐어요. from __future__ import annotations가 있으면 여전히 문자열화된 애너테이션이 쓰이지만, 이 동작은 결국 제거될 거예요.

클래스 (Classes)

class annotationlib.Format

애너테이션이 반환될 수 있는 형식을 설명하는 IntEnum이에요. enum의 멤버나 그에 해당하는 정수 값을 get_annotations()와 이 모듈의 다른 함수, 그리고 __annotate__ 함수에 넘길 수 있어요.

  • VALUE = 1 — 값은 애너테이션 표현식의 평가 결과예요.
  • VALUE_WITH_FAKE_GLOBALS = 2 — annotate 함수가 가짜 전역(fake globals)이 있는 특별한 환경에서 평가되고 있음을 알리는 특수 값이에요. 이 값을 받으면 annotate 함수는 Format.VALUE 형식과 같은 값을 반환하거나, 이 환경에서의 실행을 지원하지 않는다는 뜻으로 NotImplementedError를 발생시켜야 해요. 이 형식은 내부적으로만 쓰이고 이 모듈의 함수에 넘기면 안 돼요.
  • FORWARDREF = 3 — 정의된 값은 실제 애너테이션 값(Format.VALUE 형식대로), 정의되지 않은 값은 ForwardRef 프록시예요. 실제 객체들은 ForwardRef 프록시 객체에 대한 참조를 포함할 수 있어요.
  • STRING = 4 — 값은 애너테이션이 소스 코드에 나타나는 그대로의 텍스트 문자열이에요. 공백 정규화와 상수값 최적화 같은 수정은 포함될 수 있어요. 이 문자열들의 정확한 값은 향후 Python 버전에서 바뀔 수 있어요.

3.14 버전에서 추가.

class annotationlib.ForwardRef

애너테이션의 전방 참조를 위한 프록시 객체예요.

FORWARDREF 형식을 사용할 때 애너테이션이 해결할 수 없는 이름을 포함하면 이 클래스의 인스턴스가 반환돼요. 예를 들어 클래스가 정의되기 전에 참조되는 전방 참조가 애너테이션에 쓰였을 때 발생할 수 있어요.

  • __forward_arg__ForwardRef를 생성하도록 평가된 코드를 담은 문자열이에요. 원래 소스와 정확히 같지 않을 수 있어요.

  • evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE) — 전방 참조를 평가해서 그 값을 반환해요. formatVALUE(기본값)이면, 전방 참조가 해결할 수 없는 이름을 가리킬 때 NameError 같은 예외를 던질 수 있어요. 이 메서드의 인자들로 원래는 정의되지 않을 이름에 대한 바인딩을 제공할 수 있어요. formatFORWARDREF면 예외를 던지지 않고 ForwardRef 인스턴스를 반환할 수 있어요. 예를 들어 전방 참조 객체가 list[undefined] 코드를 담고 있고 undefined가 정의되지 않은 이름이라면, FORWARDREF 형식으로 평가하면 list[ForwardRef('undefined')]를 반환해요. formatSTRING이면 __forward_arg__를 반환해요.

    owner 매개변수는 이 메서드에 스코프 정보를 넘기는 기본 메커니즘이에요. ForwardRef의 owner는 그 ForwardRef가 파생된 애너테이션을 담고 있는 객체, 즉 모듈 객체, 타입 객체, 함수 객체예요.

    globals, locals, type_params 매개변수는 ForwardRef가 평가될 때 사용할 수 있는 이름에 더 정밀하게 영향을 주는 메커니즘이에요. globalslocalseval()에 전달돼, 이름이 평가되는 전역·지역 네임스페이스를 나타내요. type_params는 제네릭 클래스와 함수의 네이티브 문법으로 만들어진 객체와 관련돼요. 전방 참조가 평가되는 동안 스코프에 있는 타입 매개변수의 튜플이에요. 예를 들어 제네릭 클래스 C의 클래스 네임스페이스에 있는 애너테이션에서 가져온 ForwardRef를 평가한다면, type_paramsC.__type_params__로 설정해야 해요.

    get_annotations()가 반환한 ForwardRef 인스턴스는 자신이 유래한 스코프에 대한 정보의 참조를 유지해요. 그래서 추가 인자 없이 이 메서드를 호출해도 그런 객체를 평가할 수 있어요. 다른 방식으로 만든 ForwardRef 인스턴스는 스코프에 대한 정보가 없을 수 있으니, 성공적으로 평가하려면 이 메서드에 인자를 넘겨야 할 수 있어요.

    owner, globals, locals, type_params가 모두 주어지지 않고 ForwardRef가 자기 기원에 대한 정보도 담고 있지 않으면, 빈 globalslocals 사전이 사용돼요.

    3.14 버전에서 추가.

함수 (Functions)

annotationlib.annotations_to_string(annotations)

런타임 값을 담은 애너테이션 사전을, 문자열만 담은 사전으로 변환해요. 값이 이미 문자열이 아니면 type_repr()을 사용해 변환해요. STRING 형식을 지원하지만 애너테이션을 만드는 코드에 접근할 수 없는, 사용자가 제공한 annotate 함수를 위한 헬퍼예요. 예를 들어 함수형 문법으로 만든 typing.TypedDict 클래스의 STRING 형식을 구현하는 데 쓰여요.

>>> from typing import TypedDict
>>> Movie = TypedDict("movie", {"name": str, "year": int})
>>> get_annotations(Movie, format=Format.STRING)
{'name': 'str', 'year': 'int'}

3.14 버전에서 추가.

annotationlib.call_annotate_function(annotate, format, *, owner=None)

주어진 format(Format enum의 멤버)으로 annotate 함수 annotate를 호출하고, 함수가 만들어낸 애너테이션 사전을 반환해요.

이 헬퍼 함수가 필요한 이유는, 컴파일러가 함수·클래스·모듈용으로 생성한 annotate 함수는 직접 호출하면 VALUE 형식만 지원하기 때문이에요. 다른 형식을 지원하기 위해 이 함수는 annotate 함수를 특별한 환경에서 호출해서 다른 형식으로 애너테이션을 만들 수 있게 해요. 클래스가 구성되는 동안 애너테이션을 부분적으로 평가해야 하는 기능을 구현할 때 유용한 빌딩 블록이에요.

owner는 애너테이션 함수를 소유한 객체(보통 함수, 클래스, 모듈)예요. 주어지면 FORWARDREF 형식에서 더 많은 정보를 담은 ForwardRef 객체를 만드는 데 사용돼요.

더 알아보기 — 이 함수가 쓰는 구현 기법에 대한 설명은 PEP 649 에 있어요.

3.14 버전에서 추가.

annotationlib.call_evaluate_function(evaluate, format, *, owner=None)

주어진 format(Format enum의 멤버)으로 evaluate 함수 evaluate를 호출하고 함수가 만들어낸 값을 반환해요. call_annotate_function()과 비슷하지만, 후자는 항상 문자열을 애너테이션에 매핑하는 사전을 반환하는 반면 이 함수는 단일 값을 반환해요.

이 함수는 타입 별칭과 타입 매개변수와 관련된 지연 평가 요소를 위해 생성된 evaluate 함수와 함께 쓰도록 설계됐어요.

  • typing.TypeAliasType.evaluate_value() — 타입 별칭의 값
  • typing.TypeVar.evaluate_bound() — 타입 변수의 bound
  • typing.TypeVar.evaluate_constraints() — 타입 변수의 constraints
  • typing.TypeVar.evaluate_default() — 타입 변수의 기본값
  • typing.ParamSpec.evaluate_default() — 파라미터 명세의 기본값
  • typing.TypeVarTuple.evaluate_default() — 타입 변수 튜플의 기본값

owner는 evaluate 함수를 소유한 객체, 예를 들어 타입 별칭이나 타입 변수 객체예요. format은 값이 반환되는 형식을 제어해요.

>>> type Alias = undefined
>>> call_evaluate_function(Alias.evaluate_value, Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'undefined' is not defined
>>> call_evaluate_function(Alias.evaluate_value, Format.FORWARDREF)
ForwardRef('undefined')
>>> call_evaluate_function(Alias.evaluate_value, Format.STRING)
'undefined'

3.14 버전에서 추가.

annotationlib.get_annotate_from_class_namespace(namespace)

클래스 네임스페이스 사전 namespace에서 annotate 함수를 가져와요. 네임스페이스에 annotate 함수가 없으면 None을 반환해요. 주로 클래스가 완전히 만들어지기 전(예: 메타클래스에서)에 유용해요. 클래스가 존재한 뒤에는 cls.__annotate__로 annotate 함수를 가져올 수 있어요. 메타클래스에서 이 함수를 쓰는 예시는 아래에 있어요.

3.14 버전에서 추가.

annotationlib.get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)

객체의 애너테이션 사전을 계산해요.

obj는 callable, 클래스, 모듈, 혹은 __annotate__ 또는 __annotations__ 속성을 가진 다른 객체일 수 있어요. 다른 객체를 넘기면 TypeError가 발생해요.

format 매개변수는 애너테이션이 반환되는 형식을 제어하고, Format enum의 멤버나 그에 해당하는 정수여야 해요. 각 형식은 이렇게 작동해요.

  • VALUE: object.__annotations__를 먼저 시도하고, 존재하지 않으면 object.__annotate__ 함수를 호출해요(존재한다면).
  • FORWARDREF: object.__annotations__가 존재하고 성공적으로 평가될 수 있으면 그것을 사용하고, 아니면 object.__annotate__ 함수를 호출해요. 그것도 존재하지 않으면 object.__annotations__를 다시 시도하고, 접근 중 발생한 오류를 다시 발생시켜요. object.__annotate__를 호출할 때는 먼저 FORWARDREF로 호출해요. 구현되지 않았다면 VALUE_WITH_FAKE_GLOBALS가 지원되는지 확인하고 가짜 전역 환경에서 그것을 사용해요. 두 형식 모두 지원되지 않으면 VALUE를 사용하는 것으로 넘어가요. VALUE가 실패하면 이 호출의 오류가 발생해요.
  • STRING: object.__annotate__가 존재하면 먼저 호출하고, 아니면 object.__annotations__를 사용해 annotations_to_string()으로 문자열화해요. object.__annotate__를 호출할 때는 먼저 STRING으로 호출해요. 구현되지 않았다면 VALUE_WITH_FAKE_GLOBALS가 지원되는지 확인하고 가짜 전역 환경에서 그것을 사용해요. 두 형식 모두 지원되지 않으면 VALUE를 사용하고 결과를 annotations_to_string()으로 변환해요. VALUE가 실패하면 이 호출의 오류가 발생해요.

dict를 반환해요. get_annotations()는 호출할 때마다 새 dict를 반환해요. 같은 객체에 두 번 호출하면 서로 다르지만 동등한 dict 두 개를 반환하는 거죠.

이 함수는 여러 세부 사항을 자동으로 처리해줘요.

  • eval_str이 참이면 str 타입의 값이 eval()로 언스트링화(un-stringize)돼요. 문자열화된 애너테이션(from __future__ import annotations)과 함께 쓰려는 용도예요. Format.VALUE 이외의 형식에서 eval_str을 참으로 설정하는 것은 오류예요.
  • obj에 애너테이션 사전이 없으면 빈 dict를 반환해요. (함수와 메서드는 항상 애너테이션 사전을 가지고, 클래스·모듈·다른 종류의 callable은 없을 수 있어요.)
  • 클래스의 상속된 애너테이션과 메타클래스의 애너테이션은 무시해요. 클래스가 자기 자신의 애너테이션 사전이 없으면 빈 dict를 반환해요.
  • 객체 멤버와 dict 값에 대한 모든 접근은 안전을 위해 getattr()dict.get()을 사용해요.

eval_strstr 타입의 값이 eval() 호출 결과로 대체되는지 여부를 제어해요.

  • eval_str이 참이면 str 타입의 값에 eval()을 호출해요. (get_annotations()는 예외를 잡지 않아요. eval()이 예외를 발생시키면 get_annotations() 호출을 지나 스택을 풀어요.)
  • eval_str이 거짓(기본값)이면 str 타입의 값은 그대로예요.

globalslocalseval()에 전달돼요. 자세한 내용은 eval() 문서를 보세요. globalslocalsNone이면, 이 함수는 type(obj)에 따라 문맥별 기본값으로 그 값을 대체할 수 있어요.

  • obj가 모듈이면 globals는 기본적으로 obj.__dict__예요.
  • obj가 클래스이면 globals는 기본적으로 sys.modules[obj.__module__].__dict__, locals는 기본적으로 obj 클래스 네임스페이스예요.
  • obj가 callable이면 globals는 기본적으로 obj.__globals__예요. 다만 obj가 래핑된 함수(functools.update_wrapper() 사용)나 functools.partial 객체라면, 래핑되지 않은 함수를 찾을 때까지 언랩해요.

어떤 객체의 애너테이션 사전에 접근할 때 get_annotations()를 호출하는 것이 모범 사례예요. 애너테이션 모범 사례에 대한 자세한 내용은 Annotations Best Practices 를 보세요.

>>> def f(a: int, b: str) -> float:
...     pass
>>> get_annotations(f)
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}

3.14 버전에서 추가.

annotationlib.type_repr(value)

임의의 Python 값을 STRING 형식에 쓰기 적합한 형식으로 변환해요. 대부분의 객체에 repr()을 호출하지만, 타입 객체 같은 일부 객체는 특별히 처리해요. STRING 형식을 지원하지만 애너테이션을 만드는 코드에 접근할 수 없는, 사용자가 제공한 annotate 함수를 위한 헬퍼예요. 애너테이션에서 흔히 만나는 값을 담은 다른 객체에 사용자 친화적인 문자열 표현을 제공하는 데도 쓸 수 있어요.

3.14 버전에서 추가.

레시피 (Recipes)

메타클래스에서 애너테이션 사용하기

메타클래스는 클래스 생성 중에 클래스 본문의 애너테이션을 검사하거나 수정하고 싶을 수 있어요. 그러려면 클래스 네임스페이스 사전에서 애너테이션을 가져와야 해요. from __future__ import annotations로 만든 클래스는 애너테이션이 사전의 __annotations__ 키에 있어요. 애너테이션이 있는 다른 클래스는 get_annotate_from_class_namespace()로 annotate 함수를, call_annotate_function()으로 그것을 호출해 애너테이션을 가져올 수 있어요. 보통 FORWARDREF 형식을 쓰는 게 가장 좋아요. 클래스가 만들어질 때 아직 해결할 수 없는 이름을 애너테이션이 참조할 수 있게 해 주거든요.

애너테이션을 수정하려면 원래 annotate 함수를 호출하고 필요한 조정을 한 다음 결과를 반환하는 래퍼 annotate 함수를 만드는 게 가장 좋아요.

아래는 클래스에서 모든 typing.ClassVar 애너테이션을 걸러내 별도 속성에 넣는 메타클래스 예시예요.

import annotationlib
import typing

class ClassVarSeparator(type):
   def __new__(mcls, name, bases, ns):
      if "__annotations__" in ns:  # from __future__ import annotations
         annotations = ns["__annotations__"]
         classvar_keys = {
            key for key, value in annotations.items()
            # Use string comparison for simplicity; a more robust solution
            # could use annotationlib.ForwardRef.evaluate
            if value.startswith("ClassVar")
         }
         classvars = {key: annotations[key] for key in classvar_keys}
         ns["__annotations__"] = {
            key: value for key, value in annotations.items()
            if key not in classvar_keys
         }
         wrapped_annotate = None
      elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
         annotations = annotationlib.call_annotate_function(
            annotate, format=annotationlib.Format.FORWARDREF
         )
         classvar_keys = {
            key for key, value in annotations.items()
            if typing.get_origin(value) is typing.ClassVar
         }
         classvars = {key: annotations[key] for key in classvar_keys}

         def wrapped_annotate(format):
            annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
            return {key: value for key, value in annos.items() if key not in classvar_keys}

      else:  # no annotations
         classvars = {}
         wrapped_annotate = None
      typ = super().__new__(mcls, name, bases, ns)

      if wrapped_annotate is not None:
         # Wrap the original __annotate__ with a wrapper that removes ClassVars
         typ.__annotate__ = wrapped_annotate
      typ.classvars = classvars  # Store the ClassVars in a separate attribute
      return typ

커스텀 callable annotate 함수 만들기

커스텀 annotate 함수는 함수·클래스·모듈을 위해 자동 생성되는 것 같은 리터럴 함수일 수 있어요. 또는 클래스가 제공하는 캡슐화를 활용하고 싶을 수도 있는데, 그 경우 어떤 callable이든 annotate 함수로 쓸 수 있어요.

VALUE, STRING, FORWARDREF 형식을 직접 제공하려면, annotate 함수는 다음 속성을 제공해야 해요.

  • 시그니처가 __call__(format, /) -> dict인 callable __call__ — 지원하는 형식으로 호출될 때 NotImplementedError를 발생시키지 않아요.

VALUE_WITH_FAKE_GLOBALS 형식(직접 지원하지 않을 때 STRING이나 FORWARDREF를 자동 생성하는 데 사용)을 제공하려면 annotate 함수는 다음 속성을 제공해야 해요.

  • 시그니처가 __call__(format, /) -> dict인 callable __call__VALUE_WITH_FAKE_GLOBALS로 호출될 때 NotImplementedError를 발생시키지 않아요.
  • annotate 함수의 컴파일된 코드를 담은 code 객체 __code__.
  • 선택: __code__가 나타내는 함수가 위치 기본값을 쓰면, 함수의 위치 기본값의 튜플 __kwdefaults__.
  • 선택: __code__가 나타내는 함수가 키워드 기본값을 쓰면, 함수의 키워드 기본값의 dict __defaults__.
  • 선택: 기타 모든 함수 속성.
class Annotate:
    called_formats = []

    def __call__(self, format=None, /, *, _self=None):
        # When called with fake globals, `_self` will be the
        # actual self value, and `self` will be the format.
        if _self is not None:
            self, format = _self, self

        self.called_formats.append(format)
        if format <= 2:  # VALUE or VALUE_WITH_FAKE_GLOBALS
            return {"x": MyType}
        raise NotImplementedError

    __code__ = __call__.__code__
    __defaults__ = (None,)
    __kwdefaults__ = property(lambda self: dict(_self=self))

    __globals__ = {}
    __builtins__ = {}
    __closure__ = None

이것은 이렇게 호출할 수 있어요.

>>> from annotationlib import call_annotate_function, Format
>>> call_annotate_function(Annotate(), format=Format.STRING)
{'x': 'MyType'}

또는 객체의 annotate 함수로 쓸 수 있어요.

>>> from annotationlib import get_annotations, Format
>>> class C:
...   pass
>>> C.__annotate__ = Annotate()
>>> get_annotations(Annotate(), format=Format.STRING)
{'x': 'MyType'}

STRING 형식의 한계

STRING 형식은 애너테이션의 소스 코드를 근사하려고 하지만, 사용되는 구현 전략 때문에 정확한 소스 코드를 항상 복구할 수는 없어요.

첫째, 문자열화기(stringifier)는 컴파일된 코드에 없는 정보 — 주석, 공백, 괄호, 컴파일러가 단순화하는 연산 — 는 당연히 복구할 수 없어요.

둘째, 문자열화기는 어떤 스코프에서 이름을 찾는 거의 모든 연산을 가로챌 수 있지만, 완전히 상수에 대해 동작하는 연산은 가로챌 수 없어요. 결과적으로 신뢰할 수 없는 코드에 STRING 형식을 요청하는 건 안전하지 않아요. Python은 충분히 강력해서 전역이나 내장에 접근할 수 없어도 임의 코드 실행을 달성할 수 있거든요. 예를 들면:

>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.STRING)
Hello world
{'x': 'None'}

참고: 이 특정 예시는 (글을 쓰는 시점에) 동작하지만, 구현 세부 사항에 의존하므로 미래에 동작한다는 보장은 없어요.

ast 모듈로 표현되는 Python의 다양한 표현식 중 일부는 지원돼요. 즉 STRING 형식이 일반적으로 원본 소스 코드를 복구할 수 있다는 뜻이에요. 나머지는 지원되지 않아 잘못된 출력이나 오류가 발생할 수 있어요.

지원되는 것(가끔 주의점 포함):

ast.BinOp, ast.UnaryOp (ast.Invert(~), ast.UAdd(+), ast.USub(-)는 지원, ast.Not(not)은 미지원), ast.Dict(** 언패킹 사용 시 제외), ast.Set, ast.Compare (ast.Eqast.NotEq 지원, ast.Lt, ast.LtE, ast.Gt, ast.GtE는 지원하되 피연산자가 뒤집힐 수 있음, ast.Is, ast.IsNot, ast.In, ast.NotIn은 미지원), ast.Call(** 언패킹 사용 시 제외), ast.Constant(정확한 표현은 아님 — 예: 문자열의 이스케이프 시퀀스는 사라지고, 16진수는 10진수로 변환), ast.Attribute(값이 상수가 아닐 때), ast.Subscript(값이 상수가 아닐 때), ast.Starred(* 언패킹), ast.Name, ast.List, ast.Tuple, ast.Slice.

지원되지 않지만 문자열화기가 만나면 정보성 있는 오류를 던지는 것:

ast.FormattedValue(f-strings; !r 같은 변환 지정자를 쓰면 오류가 감지되지 않음), ast.JoinedStr(f-strings).

지원되지 않고 잘못된 출력을 내는 것:

ast.BoolOp(andor), ast.IfExp, ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp.

애너테이션 스코프에서 허용되지 않아 관련이 없는 것:

ast.NamedExpr(:=), ast.Await, ast.Yield, ast.YieldFrom.

FORWARDREF 형식의 한계

FORWARDREF 형식은 가능한 한 실제 값을 만들려고 하고, 해결할 수 없는 것은 ForwardRef 객체로 대체해요. STRING 형식과 대체로 같은 한계의 영향을 받아요. 리터럴에 대한 연산을 수행하거나 지원되지 않는 표현식 타입을 쓰는 애너테이션은 FORWARDREF 형식으로 평가할 때 예외를 발생시킬 수 있어요.

지원되지 않는 표현식에서의 동작 예시를 몇 개 볼게요.

>>> from annotationlib import get_annotations, Format
>>> def zerodiv(x: 1 / 0): ...
>>> get_annotations(zerodiv, format=Format.STRING)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> get_annotations(zerodiv, format=Format.FORWARDREF)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> def ifexp(x: 1 if y else 0): ...
>>> get_annotations(ifexp, format=Format.STRING)
{'x': '1'}

애너테이션을 들여다볼 때의 보안 의미

이 모듈의 많은 기능은 애너테이션과 관련된 코드를 실행하는 것을 포함하고, 그 코드는 임의의 일을 할 수 있어요. 예를 들어 get_annotations()는 임의의 annotate 함수를 호출할 수 있고, ForwardRef.evaluate()는 임의의 문자열에 eval()을 호출할 수 있어요. 애너테이션에 담긴 코드는 임의의 시스템 호출을 하거나, 무한 루프에 빠지거나, 그 밖의 어떤 연산도 수행할 수 있어요. 이는 __annotations__ 속성에 대한 어떤 접근에도, 그리고 typing.get_type_hints()처럼 애너테이션을 다루는 typing 모듈의 여러 함수에도 마찬가지로 해당돼요.

이로부터 발생하는 어떤 보안 문제도, 신뢰할 수 없는 애너테이션을 포함할 수 있는 코드를 import한 직후에 적용돼요. import는 언제나 임의의 연산을 수행하게 할 수 있으니까요. 하지만 신뢰할 수 없는 출처의 문자열이나 다른 입력을 받아 애너테이션 조사 API 중 하나에 넘기는 것은 안전하지 않아요. 예를 들어 __annotations__ 사전을 편집하거나 ForwardRef 객체를 직접 만드는 식으로요.