Tuple[()]

Python
공개

2026-01-18

초록

파이썬 튜플에서 만난 싱크홀.

()

파이썬에서 () 는 길이가 0 인 튜플의 리터럴 표현이다. 빈 튜플이라고 부른다. tuple() 이나 tuple([]) 등의 표현으로도 빈 튜플을 만들 수 있다. 빈 튜플은 모두 () 와 같아서 == 연산자로 비교하면 항상 True 를 얻게된다. 불변이라서 빈 튜플은 다른 값으로 바뀔 수도 없다. 그러니 싱글턴(singleton)으로 만들고 싶다는 충동이 생길 수도 있겠다. 싱글턴이 된다는 것은, None 을 다룰 때 처럼, 비교할 때 == 대신 is 를 써도 좋다는 뜻이다. 실제로 CPython 에서는 싱글턴이다.1

>>> x = ()
>>> x is tuple([])
... True

하지만 이 성질은 CPython 에서 그렇다는 것일 뿐, 파이썬 언어가 요구하는 성질은 아니다.

파이썬 토론장에 올라온 토픽이 눈에 띈다.

Should we specify in the language reference that the empty tuple is a singleton?

이 성질을 언어 요구사항으로 강제할 것인지에 대한 토론으로 시작하더니, 싱글톤 성질을 활용해서 () 를 센티널(sentinel)로 사용하는 것의 위험성을 언급한다. 센티널이란 “지정하지 않았음”, “값이 없음”, “스트림의 끝에 도달했음” 등을 알리는데 사용하는 특별한 값을 뜻한다. 센티널은 센티널을 제외한 가능한 모든 값과 구분되는 특별한 값이어야 한다. 전에는 () 를 센티널로 사용하려는 사람들이 있다고 상상해본 적도 없다. 하지만 한가지 지적하자면, 빈 튜플인지 검사할 때, is 사용이 위험한 것이지 == 사용이 문제될 것은 없다.

필자의 센티널 스타일은 이렇다.

  • 가능하면 None 을 쓰자.
  • 그럴 수 없다면, object() 로 센티널 객체를 만들자.2

Tuple[()]

빈 튜블의 형을 표현하는 방법은 Tuple[()] 이다. 3.9 부터는 그냥 tuple[()] 을 사용해도 된다.

from typing import Tuple

empty_tuple: Tuple[()] = ()

길이가 2 인 튜플의 예를 들면 Tuple[float, float]. 길이가 1 인 튜플의 예를 들면 Tuple[int]. 이 패턴을 따르자면 길이가 0 인 튜플은 Tuple[] 이 되겠지만, 파이썬에서 이런 표현은 허락하지 않는다. 그렇다고 [] 없는 Tuple 을 쓸 수도 없는데, 이 표현은 모든 종류의 튜플을 가리지 않고 허용하겠다는 뜻이기 때문이다. 그래서 꽁수로 Tuple[()] 이라는 특수한 표현을 빈 튜플을 나타내는데 사용하기로 했다.

아마 꼭 필요한 사례를 찾기는 힘들 것이다. 하지만 여러분이, 분모가 0 이 되는 조건을 따져보지 않았다고, 자다가 벌떡 일어나는 사람이라면, 이 표현 없이 살 수는 없다.

get_args

그런데, 파이썬 표준 라이브러리에서 () 를 센티널로 사용하는 곳이 있다. 바로 typing.get_args 다.

3.8: get_args(Tuple[()]) == ((),)

get_args 는 제네릭 형의 형 인자를 추출하는 함수인데, 파이썬 3.8 에서 처음 등장했다. 가령 get_args(Tuple[float, float]) == (float, float) 가 성립한다. 즉 형 인자를 튜플로 반환한다. 문제는 형 인자가 없는 경우에 사용하면 예외를 일으키거나 None 을 반환하는 대신 () 를 반환한다는 것이다. 즉 get_args(Tuple) == () 가 성립한다. 마음에 들지는 않지만, 참아줄 수는 있다. 반환 값이 언제나 튜플이라서 검사 없이 언패킹할 수 있는 편리함도 있다.

파이썬 문법은 Tuple[] 같은 표현을 허락하지 않아서, () 가 정상적인 값일 수는 없다. 빈 튜플 형에 대해 적용해보면 특별한 예외 규칙 없이 ((),) 를 반환한다. 따라서 () 가 반환된다면 형 인자가 지정되지 않았다고 판단해도 좋다.

3.9, 3.10: get_args(tuple[()]) == ()

하지만 3.9 에서 이 진술이 무너진다. 이제 제네릭 형에도 Tuple 대신 tuple 을 쓸 수 있게 되었다. 따라서 빈 튜플을 표현하는 방법으로 tuple[()] 도 가능해졌다. 하지만 무슨 이유에서인지 get_args(tuple[()]) == () 이 된 것이다. Tuple 은 여전히 3.8 처럼 동작한다. 즉, Tuple[()]tuple[()]get_args 에 대해 다른 값을 반환하기 시작했다. 적어도 get_args 로는 tupletuple[()] 을 구별할 수 없다.

이 어색한 상황은 3.10 까지 계속된다.

3.11+: get_args(Tuple[()]) == ()

그러다가 3.11 에서 바뀐다. What’s New 에 다음과 같은 구절이 있다.

The representation of empty tuple types (Tuple[()]) is simplified. This affects introspection, e.g. get_args(Tuple[()]) now evaluates to () instead of ((),). (Contributed by Serhiy Storchaka in gh-91137.)3

반대의 변경이라면 좋았겠지만, 이제 tuple[()]Tuple[()] 의 동작이 같아지기는 했다. 이 결과가 3.14 까지 이어지고 있다. 우리는 get_args 로는 Tuple[()]Tuple 을 구분할 수 없는 세상에서 살게되었다.

대단히 큰 문제라고 볼 수는 없다. __args__ 어트리뷰트라는 대안도 있으니 피해가는 것도 어렵지 않다. 하지만 쾌적한 운전을 방해하는 자그마한 싱크홀 정도는 된다.

각주

  1. 글 쓰는 시점의 CPython 최신 안정판은 3.14 다.↩︎

  2. dataclasses.MISSING 처럼. 하지만 이 방법 만으로는 프로세스 경계를 건널 수 없다.↩︎

  3. gh-91137 은 Empty typing.Tuple 이다.↩︎