Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 830 – 예외와 트레이스백에 타임스탬프 추가

Author:
Gregory P. Smith <greg at krypto.org>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Created:
15-Mar-2026
Python-Version:
3.16
Post-History:
12-Apr-2026, 18-Apr-2026

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 예외가 인스턴스화된 시점을 기록하는 선택적 __timestamp_ns__ 속성을 BaseException에 추가합니다. 이 기록에는 관측 가능한 오버헤드가 없습니다. 환경 변수나 명령줄 플래그로 활성화하면, 포맷된 트레이스백이 예외 메시지와 함께 이 타임스탬프를 표시합니다.

동기

예외 그룹(PEP 654)이 도입되면서 이제 Python 프로그램은 서로 관련 없는 여러 예외를 동시에 전파할 수 있습니다. 이러한 예외를 디버깅하거나 예외를 외부 로그 및 메트릭과 연관 지을 때, 각 예외가 언제 발생했는지 아는 것은 무엇이 발생했는지 아는 것만큼 중요한 경우가 많습니다. 이는 프로덕션 환경에서 문제를 진단할 때 흔히 겪는 어려움입니다.

현재는 이 정보를 얻는 표준적인 방법이 없습니다. Python 작성자는 예외 메시지에 시각 정보를 직접 추가하거나 로깅 프레임워크에 의존해야 하는데, 이 방식은 비용이 클 수 있고 일관성 없이 이루어지며 오류가 발생하기 쉽습니다.

여러 백엔드에서 동시에 데이터를 가져오고, 빠르게 실패하는 대신 모든 실패를 보고하는 비동기 서비스를 생각해 보십시오. 그 결과로 생성되는 ExceptionGroup에는 모든 오류가 제출 순서대로 담기며, 각 오류가 언제 발생했는지는 전혀 알 수 없습니다:

import asyncio

async def fetch_user(uid):
    await asyncio.sleep(0.5)
    raise ConnectionError(f"User service timeout for {uid}")

async def fetch_orders(uid):
    await asyncio.sleep(0.1)
    raise ValueError(f"Invalid user_id format: {uid}")

async def fetch_recommendations(uid):
    await asyncio.sleep(2.3)
    raise TimeoutError("Recommendation service timeout")

async def get_dashboard(uid):
    results = await asyncio.gather(
        fetch_user(uid),
        fetch_orders(uid),
        fetch_recommendations(uid),
        return_exceptions=True,
    )
    errors = [r for r in results if isinstance(r, Exception)]
    if errors:
        raise ExceptionGroup("dashboard fetch failed", errors)

asyncio.run(get_dashboard("usr_12@34"))

PYTHON_TRACEBACK_TIMESTAMPS=iso를 설정하면 출력은 다음과 같이 바뀝니다:

+ Exception Group Traceback (most recent call last):
|   File "service.py", line 26, in <module>
|     asyncio.run(get_dashboard("usr_12@34"))
|   ...
|   File "service.py", line 24, in get_dashboard
|     raise ExceptionGroup("dashboard fetch failed", errors)
| ExceptionGroup: dashboard fetch failed (3 sub-exceptions) <@2026-04-19T07:24:31.102431Z>
+-+---------------- 1 ----------------
  | Traceback (most recent call last):
  |   File "service.py", line 5, in fetch_user
  |     raise ConnectionError(f"User service timeout for {uid}")
  | ConnectionError: User service timeout for usr_12@34 <@2026-04-19T07:24:29.300461Z>
  +---------------- 2 ----------------
  | Traceback (most recent call last):
  |   File "service.py", line 9, in fetch_orders
  |     raise ValueError(f"Invalid user_id format: {uid}")
  | ValueError: Invalid user_id format: usr_12@34 <@2026-04-19T07:24:28.899918Z>
  +---------------- 3 ----------------
  | Traceback (most recent call last):
  |   File "service.py", line 13, in fetch_recommendations
  |     raise TimeoutError("Recommendation service timeout")
  | TimeoutError: Recommendation service timeout <@2026-04-19T07:24:31.102394Z>
  +------------------------------------

하위 예외는 제출 순서대로 나열되지만, 타임스탬프를 보면 실제로는 주문 검증이 가장 먼저 실패했고(28.899초), 사용자 서비스가 0.5초 뒤에 실패했으며(29.300초), 추천 서비스가 2.3초 뒤에 마지막으로 실패했음(31.102초)을 알 수 있습니다. 이 정보를 메트릭 대시보드, 로드 밸런서 로그, 다른 서비스의 트레이스, 또는 프로그램 자체의 로그와 연관 지어 전체 상황을 파악할 수도 있습니다.

명세

예외 타임스탬프 속성

BaseException에 새로운 읽기/쓰기 속성 __timestamp_ns__가 추가됩니다. 이 속성은 UTC 기준 Unix 에포크 이후의 나노초를 (time.time_ns()와 동일한 의미와 정밀도로) 멤버 디스크립터를 통해 노출되는 C int64_t로 저장합니다. 타임스탬프가 비활성화된 경우, 제어 흐름 예외인 경우(아래 참조), 또는 시계를 읽는 데 실패한 경우에는 값이 0입니다.

MemoryError와 같이 내부 프리 리스트에서 재사용되는 예외 인스턴스는 처음 할당될 때가 아니라 내어줄 때 타임스탬프가 기록됩니다. 인터프리터가 최후의 수단으로 사용하는 정적 MemoryError 싱글턴은 __timestamp_ns__ 값으로 0을 유지합니다.

제어 흐름 예외

일반적인 제어 흐름의 성능에 영향을 주지 않도록, 이 기능이 활성화된 경우에도 StopIteration이나 StopAsyncIteration에 대해서는 타임스탬프를 수집하지 않습니다. 이러한 예외는 이터레이션 중에 극도로 빈번하게 발생하므로, 오버헤드를 무시할 수 있는 수준으로 줄이기 위해 (isinstance가 아닌) C 타입 포인터 동일성으로 검사합니다.

hasattr, 인자 세 개짜리 getattr, dict.get, dict.setdefault, __missing__ 디스패치처럼 때때로 예외를 제어 흐름으로 사용하는 것으로 설명되는 다른 관용구들은 특별한 처리가 필요하지 않습니다. CPython의 C 내부 구현에서 이러한 핫 패스는 이미 예외 객체를 인스턴스화하지 않고 “찾을 수 없음”을 반환하기 때문입니다.

구성

이 기능은 CPython의 두 가지 표준 메커니즘을 통해 활성화됩니다:

PYTHON_TRACEBACK_TIMESTAMPS 환경 변수
나노초 정밀도의 10진수 타임스탬프를 사용하려면 ns 또는 1로, ISO 8601 UTC 형식을 사용하려면 iso로 설정하십시오. 값이 비어 있거나 설정되지 않았거나 0이면 타임스탬프가 비활성화됩니다(기본값).
-X traceback_timestamps=<format> 명령줄 옵션
같은 값을 받습니다. 환경 변수보다 우선합니다. -X traceback_timestamps가 =<format> 값 없이 지정되면 암묵적으로 =1 (나노초 정밀도 형식)을 지정한 것으로 동작합니다.

다른 CPython 설정 동작과 마찬가지로, 잘못된 환경 변수 값은 조용히 무시되지만 잘못된 -X 플래그 값은 오류가 됩니다.

PyConfig의 새 traceback_timestamps 필드는 선택된 형식을 저장하며, sys.flags.traceback_timestamps로 접근할 수 있습니다.

표시 형식

타임스탬프는 <@timestamp> 형식으로 트레이스백의 예외 메시지 줄 끝에 덧붙습니다. 이는 포맷된 트레이스백 출력에만 영향을 주며, str(exc)와 repr(exc)는 변경되지 않습니다. iso를 사용한 예:

Traceback (most recent call last):
  File "<stdin>", line 3, in california_raisin
    raise RuntimeError("not enough sunshine")
RuntimeError: not enough sunshine <@2026-04-12T18:07:30.346914Z>

색상 출력이 활성화된 경우, 타임스탬프는 예외 메시지와 시각적으로 구별되도록 흐린 색으로 표시됩니다.

ns 형식은 에포크 이후의 초를 소수점 이하 아홉 자리까지 표시하며, 예를 들면 <@1776017178.687320256>과 같습니다. iso 형식은 마이크로초 해상도로 표시합니다. 기반이 되는 __timestamp_ns__ 값은 표시 형식과 관계없이 항상 나노초 정밀도입니다.

traceback 모듈 변경 사항

TracebackException과 공개 포맷 함수들(print_exc, print_exception, format_exception, format_exception_only)에 timestamps 키워드 인자(기본값 None)가 추가됩니다:

None
전역 설정을 따릅니다. 기능이 활성화된 경우에만 설정된 형식을 사용하여 타임스탬프를 표시합니다.
False
기능이 활성화된 경우에도 타임스탬프를 절대 표시하지 않습니다.
True
전역 설정과 관계없이 0이 아닌 모든 __timestamp_ns__를 표시하며, 설정된 형식이 있으면 그 형식을, 없으면 ns를 사용합니다.

0이 아닌 값만 표시됩니다. 수집이 비활성화된 경우에도, 수집이 활성화된 프로세스에서 언피클된 인스턴스나 __timestamp_ns__가 직접 할당된 인스턴스에서는 0이 아닌 값이 나타날 수 있습니다.

포맷된 트레이스백 문자열에서 <@...> 타임스탬프 접미사를 제거하는 새 유틸리티 함수 traceback.strip_exc_timestamps(text)가 제공됩니다. 이는 트레이스백 출력을 문자 그대로 비교하는 모든 경우에 유용합니다.

doctest 변경 사항

새 doctest.IGNORE_EXCEPTION_TIMESTAMPS 옵션 플래그가 추가됩니다. 이 플래그가 활성화되면 doctest 출력 검사기가 비교 전에 실제 출력에서 타임스탬프를 제거하므로, 예외를 발생시키는 doctest가 타임스탬프 활성화 여부와 관계없이 통과합니다.

서드파티 프로젝트가 타임스탬프를 활성화한 상태로 테스트를 실행하는 것을 지원하리라고 기대하지 않으며, 그렇게 하기를 원하는 프로젝트도 많지 않을 것으로 예상합니다.

근거

타임스탬프는 BaseException C 구조체 안의 단일 int64_t 필드에 저장되며, Unix 에포크 이후 경과한 나노초를 기록합니다. 예외 노트(PEP 678)를 사용하는 대신 이 설계를 택한 이유는 다음과 같습니다. 구조체 필드는 값이 채워지지 않으면 비용이 전혀 들지 않고, 예외 발생 시점에 문자열 객체와 리스트 객체를 만들지 않으며, 모든 서식화 작업을 트레이스백 렌더링 시점으로 미룹니다. 이 기능은 전적으로 명시적으로 활성화해야만 동작하며, 예외 처리의 의미를 바꾸지 않습니다.

예외 노트를 이 정보의 전달 수단으로 사용하는 방안은 성능 오버헤드가 있고 명시적인 용도가 없어서 실현할 수 없다고 판단했습니다. 노트는 훌륭한 기능이지만, 모든 Exception 인스턴스 생성 시점에 포착되는 데이터를 수집하는 수단이 아니라 전혀 다른 사용 사례를 위해 설계되었습니다. 즉, 예외가 생길 때마다 데이터를 모으는 용도로 만들어진 것이 아닙니다.

인스턴스화 시점 대 발생 시점

타임스탬프는 예외가 발생하는 시점이 아니라 예외 객체가 생성되는 시점에 기록됩니다. 압도적으로 흔한 raise SomeError(...) 형태에서는 이 두 시점이 같습니다. 두 시점이 다른 경우에는 일반적으로 인스턴스화 시점이 더 유용한 값입니다. 단독 raise나 raise exc로 다시 발생시키면 마지막으로 다시 던져진 시점이 아니라 오류가 처음 발생한 시점이 보존되고, 예외를 생성해 모아 두었다가 나중에 ExceptionGroup으로 감싸는 코드는 각 오류가 감지된 시점을 얻습니다.

구현 측면에서도 인스턴스화 시점이 더 깔끔한 지점입니다. 예외 인스턴스화는 모두 BaseException.__init__과 그 vectorcall 경로를 거칩니다. 예외 발생에는 이에 해당하는 단일 경로가 없습니다. 가장 저수준인 _PyErr_SetRaisedException은 스레드의 예외 상태를 저장하고 복원하는 일상적인 작업에서도 호출되며, 그 위에서는 Python raise 문, C PyErr_SetObject, 직접적인 PyErr_SetRaisedException 호출이 각각 별개의 코드 경로입니다.

미해결 문제 항목의 Recording Time of First Raise를 참조하십시오.

성능 측정

pyperformance 스위트를 병합 기준 커밋, 기능을 비활성화한 PR 브랜치, 그리고 us와 iso 두 모드로 기능을 활성화한 PR 브랜치에서 실행했습니다. 이 측정은 이 PEP의 최초 버전에 해당하는 참조 구현을 대상으로, Change History에 기록된 수정 이전에 수행했습니다. 그 수정은 표시 계층을 단순화한 것뿐이므로 성능 차이는 없을 것으로 예상합니다.

유의미한 성능 변화는 관찰되지 않았습니다. 가끔 1-2%의 변동이 있었을 뿐이며, 이 변동은 안정적으로 재현되지 않았고 벤치마크 환경의 노이즈 임계값보다도 작았습니다.

제어 흐름 특수 처리를 검증하기 위해 Objects/exceptions.c에 있는 두 줄짜리 StopIteration / StopAsyncIteration 제외 처리를 제거한 상태로도 스위트를 실행했습니다. async_generators 벤치마크 하나에서만 성능 저하가 나타났으며, 일관되게 약 10% 느리게 실행되었습니다. 이 벤치마크는 대부분의 애플리케이션 동작을 반영하지 않는 사실상의 마이크로벤치마크일 가능성이 높지만, 해당 최적화의 가치를 보여 줍니다.

벤치마크는 configure --enable-optimizations로 만든 빌드를 대상으로 다음과 같은 명령을 사용해 실행했습니다:

pyperformance run -p baseline-3a7df632c96/build/python -o baseline-eopt.json
pyperformance run -p traceback-timestamps/build/python -o traceback-timestamps-default-eopt.json
PYTHON_TRACEBACK_TIMESTAMPS=1 pyperformance run --inherit-environ PYTHON_TRACEBACK_TIMESTAMPS -p traceback-timestamps/build/python -o traceback-timestamps-env=1-eopt.json
PYTHON_TRACEBACK_TIMESTAMPS=iso pyperformance run --inherit-environ PYTHON_TRACEBACK_TIMESTAMPS -p traceback-timestamps/build/python -o traceback-timestamps/Results.silencio/traceback-timestamps-env=iso-eopt.json
PYTHON_TRACEBACK_TIMESTAMPS=1 pyperformance run --inherit-environ PYTHON_TRACEBACK_TIMESTAMPS -p traceback-timestamps-without-StopIter-cases/build/python -o traceback-timestamps/Results.silencio/traceback-timestamps-without-StopIter-cases-env=1-eopt.json

하위 호환성

이 기능은 기본적으로 비활성화되어 있으며, 기존 예외 처리 코드에 영향을 주지 않습니다. __timestamp_ns__ 속성은 BaseException 인스턴스에서 항상 읽을 수 있으며, 타임스탬프를 수집하지 않을 때는 0을 반환합니다.

타임스탬프가 비활성화되어 있으면 예외는 전통적인 2-튜플 형식인 (type, args)로 피클됩니다. 0이 아닌 타임스탬프가 있으면 예외는 (type, args, state_dict)로 피클되며, 상태 딕셔너리에 __timestamp_ns__가 들어갑니다. 이전 Python 버전도 __setstate__를 통해 이를 올바르게 언피클합니다. 항상 3-튜플 형식으로(타임스탬프를 0으로 두고) 내보내면 로직이 단순해지지만, 기능이 꺼져 있을 때 피클 출력을 바이트 단위로 동일하게 유지하고 일반적인 경우의 성능 영향을 피하기 위해 그 방식을 택하지 않았습니다. 일반적으로는 더 단순한 코드가 바람직하지만, 기본 동작으로 모든 예외의 피클 크기가 커지는 것이 더 큰 위험이라고 판단했습니다.

피클된 예외의 예

트레이스백 타임스탬프 수집을 활성화한 경우:

$ build/python -X traceback_timestamps=iso -c 'import pickle; print(pickle.dumps(RuntimeError("pep-830"), protocol=pickle.HIGHEST_PROTOCOL))'
b'\x80\x05\x95L\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x0cRuntimeError\x94\x93\x94\x8c\x07pep-830\x94\x85\x94R\x94}\x94\x8c\x10__timestamp_ns__\x94\x8a\x08\xf4\xd8\x94`\x15\xaf\xa5\x18sb.'

StopIteration은 특수 처리되므로 타임스탬프 데이터를 담은 딕셔너리를 포함하지 않습니다:

$ build/python -X traceback_timestamps=iso -c 'import pickle; print(pickle.dumps(StopIteration("pep-830"), protocol=pickle.HIGHEST_PROTOCOL))'
b'\x80\x05\x95,\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\rStopIteration\x94\x93\x94\x8c\x07pep-830\x94\x85\x94R\x94.'

이 기능이 비활성화된 경우(기본값)에는 예외에 타임스탬프도 포함되지 않습니다:

$ build/python -X traceback_timestamps=0 -c 'import pickle; print(pickle.dumps(RuntimeError("pep-830"), protocol=pickle.HIGHEST_PROTOCOL))'
b'\x80\x05\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x0cRuntimeError\x94\x93\x94\x8c\x07pep-830\x94\x85\x94R\x94.'

이는 Python 3.13이 생성하는 결과와 일치합니다:

$ python3.13 -c 'import pickle; print(pickle.dumps(RuntimeError("pep-830"), protocol=pickle.HIGHEST_PROTOCOL))'
b'\x80\x05\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x0cRuntimeError\x94\x93\x94\x8c\x07pep-830\x94\x85\x94R\x94.'

유지 보수 부담

__timestamp_ns__ 필드는 BaseException C 구조체에 있는 단일 int64_t로, 설정과 관계없이 모든 예외 객체에 존재합니다. 수집 코드는 조건으로 보호되는 clock_gettime 호출이며, 포매팅 코드는 트레이스백을 표시할 때만 실행됩니다. 둘 다 작고 자체적으로 완결되어 있습니다.

지속적으로 드는 주된 비용은 테스트 스위트에 있습니다. 트레이스백 출력을 문자 그대로 비교하는 테스트는 선택적인 타임스탬프 접미사를 고려해야 합니다. 이를 위해 두 가지 헬퍼가 제공됩니다:

  • traceback.strip_exc_timestamps(text)는 포매팅된 트레이스백 문자열에서 <@...> 접미사를 제거합니다.
  • test.support.force_no_traceback_timestamps와 _test_class 접미사가 붙은 변형은 테스트 또는 TestCase 클래스가 실행되는 동안 타임스탬프 수집을 비활성화하는 데코레이터입니다.

트레이스백 전용 테스트를 제외하면, 약 1230개의 테스트 파일 중 약 14개(대략 1%)가 이 헬퍼 중 하나를 필요로 했으며, 대개 stderr를 캡처하여 예상되는 트레이스백 출력과 비교하는 테스트였습니다(예: test_logging, test_repl, test_wsgiref, test_threading). 이 패턴은 트레이스백의 ANSI 색상 코드에 대해 force_not_colorized가 사용하는 것과 같은 접근 방식을 따릅니다.

커버리지를 유지하기 위해 몇몇 GitHub Actions 실행에서 타임스탬프를 활성화하는 CPython 자체 CI를 제외하면, 대부분의 프로젝트는 테스트 스위트를 실행하는 동안 이 기능을 활성화하지 않을 가능성이 높습니다.

보안 영향

없습니다. 이 기능은 옵트인 방식이며 기본적으로 비활성화되어 있습니다.

교육 방법

__timestamp_ns__ 속성과 설정 옵션은 exceptions 모듈 레퍼런스, traceback 모듈 레퍼런스, 명령줄 인터페이스 문서에 문서화될 예정입니다.

이는 고급 사용자를 위한 기능입니다: 기본적으로 비활성화되어 있으며 명시적으로 활성화하지 않는 한 드러나지 않습니다. 입문 자료에서 다룰 필요는 없습니다.

참조 구현

CPython PR #129337.

기각된 아이디어

예외 노트 사용

타임스탬프를 첨부하기 위해 PEP 678의 .add_note()를 사용하는 방안은 여러 이유로 거부되었습니다. 노트는 예외 발생 시점에 문자열 객체와 리스트 객체를 생성해야 하므로, 타임스탬프가 표시되지 않을 때에도 오버헤드를 발생시킵니다. 예외를 포착할 때 추가된 노트는 발생 시각이 아니라 포착 시각을 반영하며, 비동기 코드에서는 이 차이가 상당할 수 있습니다. 모든 예외가 포착되는 것은 아니므로(일부는 최상위 수준까지 전파되거나 직접 로깅됨), 포착 시점의 노트는 일관성 없이 적용될 것입니다. 구조체 필드는 발생 지점에서 타임스탬프를 기록하고 모든 포매팅을 표시 시점으로 미룹니다.

sys.excepthook 사용

sys.excepthook은 잡히지 않은 예외가 최상위 수준에 도달했을 때만 실행되며, 실행 시점은 예외가 생성된 때가 아니라 표시되는 때입니다. 앞의 동기 부여 예제에서는 모든 태스크가 완료된 뒤 그 결과로 생긴 ExceptionGroup에 대해 훅이 한 번 실행되므로, 모든 하위 예외가 같은 타임스탬프를 받게 됩니다. 잡혀서 로그로 기록되는 예외는 훅에 전혀 도달하지 않습니다.

sys.monitoring 사용하기

원칙적으로는 인터프리터를 변경하지 않고도 PEP 669를 기반으로 서드파티 애드온 형태로 동등한 기능을 만들 수 있습니다. sys.monitoring.events.RAISE에 C로 구현된 호출 가능 객체를 등록하고, 이 객체가 시계를 읽어 예외에 __timestamp_ns__ 속성을 설정하거나 add_note()를 호출하게 하면 됩니다. STOP_ITERATION과 RERAISE는 별개의 이벤트이므로, RAISE만 구독하면 이터레이터의 핫 패스를 피할 수 있고 다시 발생시킬 때 원래 타임스탬프도 자연스럽게 보존됩니다.

이 방식은 구조체 필드 방식보다 비용이 더 클 것으로 예상됩니다. RAISE는 예외마다 한 번이 아니라 스택 해제 과정에서 Python 프레임마다 한 번씩 발생하므로 콜백이 작업에 필요한 것보다 더 자주 실행되며, 각 호출은 직접 호출되지 않고 vectorcall을 통해 디스패치됩니다. 구조체 필드가 없으면 값을 저장할 때 예외의 인스턴스 __dict__와 타임스탬프용 PyLongObject를 할당하게 되며, add_note()의 경우에는 딕셔너리와 함께 __notes__ 리스트와 포맷된 문자열을 할당하게 됩니다. 수가 제한된 모니터링 도구 ID 중 하나도 차지하게 됩니다. add_note() 방식은 추가 통합 없이도 트레이스백에 표시됩니다. 반면 __timestamp_ns__ 방식은 값을 표시하려면 다른 코드가 이미 사용하고 있을 수도 있는 sys.excepthook 싱글턴이나 traceback 모듈에 대한 몽키패치가 추가로 필요합니다. 프로그램에서 예외를 발생시킬 때마다 리스트와 문자열, 경우에 따라서는 인스턴스 딕셔너리까지 할당하는 것은 오버헤드 측면에서 너무 과하다고 판단되어 이 방식은 시도하지 않았습니다.

절충안으로 sys.monitoring은 수집에만 사용하고, BaseException에 두는 이 PEP의 int64_t 구조체 필드와 0이 아닌 __timestamp_ns__를 표시하는 traceback 모듈의 기능은 그대로 유지하는 방법이 있습니다. 이렇게 하면 BaseException 생성자에서 조건부 시계 읽기가 빠지므로 기능이 꺼져 있을 때 비용이 아주 조금 줄어들지만, 기능이 켜져 있을 때는 오버헤드가 눈에 띄게 커집니다.

항상 수집하기 vs. 항상 표시하기

타임스탬프를 수집하는 것(인스턴스화 중의 clock_gettime 호출)과 포맷된 트레이스백에 표시하는 것은 별개의 문제입니다.

항상 표시하는 방안은 대부분의 사용자에게 필요하지 않은 잡음을 더하므로 기각되었습니다. int64_t 필드는 어차피 구조체에 존재하므로 (표시가 비활성화되어 있을 때도) 항상 수집하는 비용은 적습니다. 하지만 수집하지 않으면 기능이 꺼져 있을 때 성능에 영향을 줄 가능성을 아예 없앨 수 있고, 현재로서는 한 번도 표시되지 않을 타임스탬프를 수집할 이유가 없습니다. 트레이스백 표시와 별개로 프로그램에서 예외 타임스탬프에 접근하는 것이 유용해진다면 이 결정을 다시 검토할 수 있습니다.

런타임 API

이것은 연산자 수준이 아닌 운영자(operator) 수준의 설정으로, 라이브러리나 애플리케이션 코드가 아니라 프로세스를 실행하는 사람이 구성하도록 의도되었습니다. 프로세스 전체에 적용되는 상태에 런타임 토글을 노출하면 프로그램의 여러 부분이 그 값을 두고 서로 다투게 될 수 있습니다. 시작 시점에 값을 고정하면 이런 문제를 피할 수 있습니다. 단순함을 유지하기 위해 현재는 이 기능을 제외했습니다. 런타임에 구성을 바꿀 수 있어야 한다는 수요가 있다면, 나중에 이 기능을 추가하는 데 걸림돌은 없습니다.

트레이스백 헤더 줄의 타임스탬프

타임스탬프를 Traceback (most recent call last): 헤더에 넣는 방안이 Type: message 줄을 파싱하는 도구에 영향을 덜 준다는 이유로 제안되었습니다. __cause__ 또는 __context__ 체인의 각 연결 고리와 ExceptionGroup의 각 하위 예외는 실제로 각자의 헤더를 가지므로, 이 방안으로 일반적인 경우는 처리할 수 있습니다. 하지만 헤더는 예외에 __traceback__이 있을 때만 출력됩니다. 생성된 뒤 직접 연결된 예외는 헤더 없이 Type: message로만 표시됩니다. 표준 라이브러리 자체도 이렇게 합니다. concurrent.futures.ProcessPoolExecutor와 multiprocessing.Pool은 워커의 포맷된 트레이스백을 프로세스 경계 너머로 전달하기 위해 직접 생성한 _RemoteTraceback을 __cause__로 연결하며, 이 예외는 헤더 없이 표시됩니다. 표시되는 모든 예외에서 확실히 나타나는 위치는 메시지 줄 끝뿐이므로, 메시지 줄에 덧붙이는 방법만이 이를 보장합니다.

읽는 순서에 관한 인자도 있습니다. 로그에서 오류를 훑어볼 때 사람들은 보통 예외 타입을 검색하여 Type: message 줄에 도달한 다음, 프레임을 따라 위쪽으로 읽어 올라갑니다. 타임스탬프를 그 줄에 넣으면, 마지막에 읽는 경우가 많은 블록의 맨 위가 아니라 눈이 가장 먼저 닿는 곳에 타임스탬프가 놓입니다.

더 명확한 timestamps 매개변수 이름

초기 초안에서는 traceback 서식 매개변수의 이름을 no_timestamp로 지었는데, 이 불리언은 이중 부정으로 읽혔습니다. allow_timestamps나 show_timestamp 같은 더 긴 대안도 검토되었습니다. 짧은 긍정형인 timestamps를 기본값이 None인(전역 설정을 따르는) 세 가지 상태 값으로 선택했으며, 정확한 동작은 독스트링과 문서에서 설명합니다.

사용자 정의 타임스탬프 형식

사용자 정의 형식 문자열은 복잡성을 크게 늘립니다. 두 가지 내장 형식(ns, iso)이 일반적인 요구 사항을 충족합니다. 즉, 프로그램에서 사용하기 위한 10진수 초 단위 값과 외부 시스템과 연관 짓기 위한 ISO 8601입니다.

설정 가능한 제어 흐름 예외 집합

사용자가 건너뛸 예외를 추가로 등록할 수 있게 하는 방안은 기각되었습니다. 제외 검사는 예외 생성의 핫 패스에서 실행되며, 속도를 위해 C 타입 포인터의 동일성을 사용합니다. 설정 가능한 집합을 지원하려면 isinstance 검사(MRO를 순회하므로 너무 느림)나 타입 포인터의 해시 집합(이점이 불분명한 복잡성)이 필요합니다. StopIteration과 StopAsyncIteration은 clock_gettime의 비용을 측정할 수 있을 만큼 높은 빈도로 발생하는 유일한 예외입니다. 실질적인 필요가 생기면 추가 제외 항목을 효율적으로 등록하는 API를 후속 개선 사항으로 추가할 수 있습니다.

밀리초 정밀도

time.time_ns()와 일치시키고, 예외가 빈번하게 발생하는 시나리오에 충분한 해상도를 제공하기 위해 밀리초가 아닌 나노초 정밀도를 선택했습니다.

별도의 밀리초 및 마이크로초 표시 형식

초기 초안에서는 ns와 함께 us 표시 형식을 제공했으며, ms 형식도 제안되었습니다. 이 형식들은 표시되는 소수 자릿수만 ns와 달랐습니다. 10진수 출력은 기계가 처리하도록 의도된 것이므로 정밀도를 잘라내도 실질적인 이점이 없습니다. 따라서 최대 정밀도의 ns 형식만 제공합니다. 사람이 읽기 쉬운 형식을 원하는 사용자는 iso를 사용하십시오.

설정되지 않았을 때 None 반환

타임스탬프가 수집되지 않았을 때 0 대신 None을 반환하는 편이 조금 더 파이썬다울 수 있지만, __timestamp_ns__는 C 구조체의 일반 int64_t 멤버 디스크립터로 노출됩니다. None을 지원하려면 사용자 정의 게터와 박싱된 저장소가 필요합니다. 0은 모호하지 않습니다.

저해상도 시계 사용

참조 구현은 표준 벽시계를 최대 해상도로 읽는 PyTime_TimeRaw를 사용합니다. 벤치마크 결과 실제로는 이 비용이 관찰되지 않습니다. CLOCK_REALTIME_COARSE 같은 저해상도 시계는 해상도가 낮아 여러 요구를 충족하지 못하면서도 측정 가능한 이점은 없습니다.

미해결 문제

표시 위치

현재 명세는 Type: message 줄에 타임스탬프를 덧붙입니다. 타임스탬프를 대신 Traceback (most recent call last): 헤더나 별도의 줄에 두자는 제안이 있었습니다. 현재 방식을 선택한 이유는 Timestamp in the Traceback Header Line을 참조하십시오. 이 부분은 변경될 수 있습니다. 위치를 설정 가능하게 만들 수도 있지만, 설정 항목을 하나 추가할 때마다 지원해야 할 복잡성이 늘어납니다.

기존 코드가 Type: message 줄을 파싱하므로 접미사가 붙으면 문제가 생길 수 있다는 우려가 제기되었습니다. Traceback 헤더에도 같은 우려가 적용됩니다. 그런 경우는 드물었지만, 참조 구현에서 doctest의 트레이스백 매처와 CPython 자체의 test.support 헬퍼도 이미 수정이 필요했습니다.

sys.monitoring 대안 벤치마크

설계와 예상 비용은 기각된 아이디어의 Using sys.monitoring 항목을 참조하십시오. 이러한 분석에도 불구하고 인터프리터 변경을 피하는 쪽을 강하게 선호한다면, 이 문제를 결론짓기 위해 프로토타입과 벤치마크를 만들 수 있습니다.

최초 발생 시각 기록

인스턴스화 시각 대신, 또는 그에 더하여 예외가 최초로 발생(raise)한 시각을 기록하자는 제안이 있었습니다. 이는 기각된 것이 아니라 보류된 사항입니다. 이 방식은 예외가 발생하기 훨씬 전에 생성되는 경우를 다룰 수 있지만, 인터프리터의 예외 발생 경로에서 깔끔한 훅 지점을 찾아야 하고(Instantiation Time vs. Raise Time 참조) 재발생(re-raise)의 의미를 정의해야 합니다. 예외를 처음 사용하기 훨씬 전에 생성하는 패턴을 흔히 사용하는 코드는 알려진 바가 없습니다.

감사의 글

최초 아이디어를 제안해 주신 Nathaniel J. Smith와 2025년에 구현에 대한 초기 리뷰 피드백을 주신 Daniel Colascione에게 감사드립니다.

변경 이력

  • 2026년 4월 18일
    • ns 표시 형식을 ns 접미사가 붙은 정수에서 소수점 이하 9자리의 초 단위 값으로 변경하여 datetime.fromtimestamp()에 직접 사용할 수 있도록 했습니다. us 형식을 제거했습니다. 이제 1로 활성화하거나 명시적인 형식 값 없이 활성화하면 ns가 기본값입니다.
    • traceback 모듈의 no_timestamp 매개변수를 세 가지 상태를 갖는 timestamps로 대체했습니다(기본값은 None이며 전역 설정을 따릅니다).
    • __timestamp_ns__가 UTC라는 점, 시계 읽기에 실패하면 0이 된다는 점, free-list 및 싱글턴 MemoryError 인스턴스에 타임스탬프가 기록되는 방식, str(exc)와 repr(exc)는 변경되지 않는다는 점, 그리고 iso 표시 형식이 마이크로초 해상도를 가진다는 점을 명확히 했습니다.
    • hasattr, 인자 세 개짜리 getattr, 딕셔너리 조회 실패 경로는 CPython의 핫 패스에서 예외를 인스턴스화하지 않으므로 특별한 처리가 필요 없다는 점을 명시했습니다.
    • 근거 절에 인스턴스화 시각과 발생 시각을 비교하는 하위 절을 추가했습니다.
    • sys.excepthook, sys.monitoring, Traceback 헤더 줄에 타임스탬프를 배치하는 방안, 설정되지 않았을 때 None을 반환하는 방안, ms/us 별도 표시 형식, 저해상도 시계 사용에 대한 기각된 아이디어 항목을 추가했고, 런타임 API 기각 사유의 문구를 수정했습니다.
    • 표시 위치, sys.monitoring 대안 벤치마크, 최초 발생 시각 기록을 다루는 미해결 문제 절을 추가했습니다.
    • 동기 절의 예제를 그 자체로 완결되도록 재작성했습니다.