본문으로 건너뛰기
개발 머꼬
개발 노트Python
hohyeon.dev29

Python 3.14부터 타입 힌트에 따옴표를 씌우지 않아도 되는 이유

  • #Engineering Note
  • #Python
  • #Typing
  • #Upgrade

문제 발생

서로를 참조하는 두 클래스에 타입 힌트를 달면 아직 정의되지 않은 이름이라 NameError가 났고, 그래서 따옴표로 감싸거나 파일 맨 위에 from __future__ import annotations를 넣는 것이 관행이었습니다.

class Order:
    def customer(self) -> "Customer":   # 따옴표가 없으면 NameError
        ...

class Customer:
    def orders(self) -> list[Order]:
        ...

문자열로 감싼 타입은 IDE와 타입 체커가 대부분 이해하지만, 런타임에 애노테이션을 읽는 코드(직렬화 라이브러리, DI 컨테이너)에서는 직접 eval을 해야 해서 다루기 번거로웠습니다.

원인 분석

3.13까지 애노테이션은 함수나 클래스가 정의되는 순간 즉시 평가됐습니다. 그 시점에 Customer가 아직 없으니 오류가 나는 것이 당연했습니다.

Python 3.14는 PEP 649와 PEP 749로 이 시점을 미룹니다. 애노테이션은 정의 시점에 평가되지 않고 별도의 annotate 함수에 담겨 있다가 실제로 조회할 때 계산됩니다. 그래서 정의 순서와 무관해집니다.

읽는 쪽에는 새 annotationlib 모듈이 세 가지 형식을 제공합니다.

  • VALUE — 평가된 실제 객체. 지금까지 __annotations__에서 기대하던 값입니다.
  • FORWARDREF — 아직 해소되지 않은 이름을 ForwardRef 객체로 돌려줍니다.
  • STRING — 소스에 적힌 그대로의 문자열.

from __future__ import annotations와는 다릅니다. 그 future import는 애노테이션을 항상 문자열로 만들어서, 런타임에 실제 타입을 알아야 하는 라이브러리가 직접 복원해야 했습니다. 3.14의 방식은 필요할 때 진짜 객체를 돌려줍니다.

해결 방안

  1. 새로 쓰는 코드에서는 따옴표와 future import를 걷어냅니다.
class Order:
    def customer(self) -> Customer:     # 3.14에서는 이대로 동작
        ...

class Customer:
    def orders(self) -> list[Order]:
        ...
  1. 런타임에 애노테이션을 읽는 코드는 annotationlib을 씁니다. __annotations__를 직접 건드리는 대신 원하는 형식을 명시하면, 아직 해소되지 않은 이름 때문에 터지는 일이 줄어듭니다.
  2. 지원 버전을 확인합니다. 3.13 이하를 함께 지원해야 한다면 따옴표나 future import는 그대로 두어야 합니다. 이 변경은 3.14 전용입니다.
  3. from __future__ import annotations가 남아 있는 파일을 파악해 둡니다. 3.14에서도 동작하지만 동작 방식이 새 기본값과 달라, 런타임 애노테이션을 쓰는 라이브러리와 섞이면 혼란의 원인이 됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.