Skip to content

Commit 75072cb

Browse files
committed
Go full on -X traceback_timestamps command line flag.
Better docs, improved tests. Claude Code using Sonnet 3.7 helped with this, but that was a bit of a battle as our CPython code context size for this type of change is huge.
1 parent 8043b80 commit 75072cb

11 files changed

Lines changed: 345 additions & 35 deletions

File tree

Doc/library/sys.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,11 @@ always available. Unless explicitly noted otherwise, all variables are read-only
594594
* - .. attribute:: flags.warn_default_encoding
595595
- :option:`-X warn_default_encoding <-X>`
596596

597+
* - .. attribute:: flags.traceback_timestamps
598+
- :option:`-X traceback_timestamps <-X>`. This is a string containing
599+
the selected format (``us``, ``ns``, ``iso``), or an empty string
600+
when disabled.
601+
597602
.. versionchanged:: 3.2
598603
Added ``quiet`` attribute for the new :option:`-q` flag.
599604

@@ -620,6 +625,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only
620625
.. versionchanged:: 3.11
621626
Added the ``int_max_str_digits`` attribute.
622627

628+
.. versionchanged:: next
629+
Added the ``traceback_timestamps`` attribute.
630+
623631

624632
.. data:: float_info
625633

Doc/library/traceback.rst

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88

99
--------------
1010

11-
This module provides a standard interface to extract, format and print
12-
stack traces of Python programs. It is more flexible than the
13-
interpreter's default traceback display, and therefore makes it
14-
possible to configure certain aspects of the output. Finally,
15-
it contains a utility for capturing enough information about an
16-
exception to print it later, without the need to save a reference
17-
to the actual exception. Since exceptions can be the roots of large
18-
objects graph, this utility can significantly improve
19-
memory management.
11+
This module provides a standard interface to extract, format and print stack
12+
traces of Python programs. While it has been around forever, it is used by
13+
default for more flexible traceback display as of Python 3.13. It enables
14+
configuring various aspects of the output. Finally, it contains utility classes
15+
for capturing enough information about an exception to print it later, without
16+
the need to save a reference to the actual exception. Since exceptions can be
17+
the roots of large objects graph, that can significantly improve memory
18+
management.
2019

2120
.. index:: pair: object; traceback
2221

@@ -48,6 +47,10 @@ The module's API can be divided into two parts:
4847
Output is colorized by default and can be
4948
:ref:`controlled using environment variables <using-on-controlling-color>`.
5049

50+
.. versionadded:: next
51+
Tracebacks can now contain timestamps. Display of which can be configured by
52+
the :envvar:`PYTHON_TRACEBACK_TIMESTAMPS` environment variable or the
53+
:option:`-X traceback_timestamps <-X>` command line option.
5154

5255
Module-Level Functions
5356
----------------------
@@ -105,8 +108,11 @@ Module-Level Functions
105108
printed as well, like the interpreter itself does when printing an unhandled
106109
exception.
107110

108-
If *no_timestamp* is ``True`` and :envvar:`PYTHON_TRACEBACK_TIMESTAMPS`
109-
is enabled, any timestamp after the exception message will be omitted.
111+
If *no_timestamp* is ``True`` and a traceback timestamp format is enabled via the
112+
:envvar:`PYTHON_TRACEBACK_TIMESTAMPS` environment variable or the
113+
:option:`-X traceback_timestamps <-X>` option, any timestamp after the exception
114+
message will be omitted. This is useful for tests or other situations where
115+
you need consistent output regardless of when exceptions occur.
110116

111117
.. versionchanged:: 3.5
112118
The *etype* argument is ignored and inferred from the type of *value*.
@@ -203,8 +209,12 @@ Module-Level Functions
203209
:exc:`BaseExceptionGroup`, the nested exceptions are included as
204210
well, recursively, with indentation relative to their nesting depth.
205211

206-
If *no_timestamp* is ``True`` and :envvar:`PYTHON_TRACEBACK_TIMESTAMPS`
207-
is enabled, any timestamp after the exception message will be omitted.
212+
If *no_timestamp* is ``True`` and a traceback timestamp formatting is enabled
213+
via the :envvar:`PYTHON_TRACEBACK_TIMESTAMPS` environment variable or the
214+
:option:`-X traceback_timestamps <-X>` command line option, any timestamp
215+
after the exception message will be omitted. This is useful for tests or
216+
other situations where you need consistent output regardless of when
217+
exceptions occur.
208218

209219
.. versionchanged:: 3.10
210220
The *etype* parameter has been renamed to *exc* and is now
@@ -230,8 +240,12 @@ Module-Level Functions
230240
containing internal newlines. When these lines are concatenated and printed,
231241
exactly the same text is printed as does :func:`print_exception`.
232242

233-
If *no_timestamp* is ``True`` and :envvar:`PYTHON_TRACEBACK_TIMESTAMPS`
234-
is enabled, any timestamp after the exception message will be omitted.
243+
If *no_timestamp* is ``True`` and a traceback timestamp formatting is enabled
244+
via the :envvar:`PYTHON_TRACEBACK_TIMESTAMPS` environment variable or the
245+
:option:`-X traceback_timestamps <-X>` command line option, any timestamp
246+
after the exception message will be omitted. This is useful for tests or
247+
other situations where you need consistent output regardless of when
248+
exceptions occur.
235249

236250
.. versionchanged:: 3.5
237251
The *etype* argument is ignored and inferred from the type of *value*.
@@ -289,9 +303,12 @@ Module-Level Functions
289303

290304
Given *output* of ``str`` or ``bytes`` presumed to contain a rendered
291305
traceback, if traceback timestamps are enabled (see
292-
:envvar:`PYTHON_TRACEBACK_TIMESTAMPS`) returns output of the same type with
293-
all formatted exception message timestamp values removed. When disabled,
294-
returns *output* unchanged.
306+
:envvar:`PYTHON_TRACEBACK_TIMESTAMPS` or the :option:`-X traceback_timestamps <-X>`
307+
option) returns output of the same type with all formatted exception message timestamp
308+
values removed. When timestamps are disabled, returns *output* unchanged.
309+
310+
This function is useful when you need to compare exception outputs or process
311+
them without the timestamp information.
295312

296313
.. versionadded:: next
297314

@@ -805,4 +822,3 @@ With the helper class, we have more options::
805822
1/0
806823
~^~
807824
ZeroDivisionError: division by zero
808-

Doc/using/cmdline.rst

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,15 @@ Miscellaneous options
628628

629629
.. versionadded:: 3.13
630630

631+
* :samp:`-X traceback_timestamps=[us|ns|iso|0|1]` enables or configures timestamp
632+
display in exception tracebacks. When enabled, each exception's traceback
633+
will include a timestamp showing when the exception occurred. The format
634+
options are: ``us`` (microseconds, default if no value provided), ``ns``
635+
(nanoseconds), ``iso`` (ISO-8601 formatted time), ``0`` (disable timestamps),
636+
and ``1`` (equivalent to ``us``). See also :envvar:`PYTHON_TRACEBACK_TIMESTAMPS`.
637+
638+
.. versionadded:: next
639+
631640
It also allows passing arbitrary values and retrieving them through the
632641
:data:`sys._xoptions` dictionary.
633642

@@ -1223,15 +1232,22 @@ conflict.
12231232

12241233
.. envvar:: PYTHON_TRACEBACK_TIMESTAMPS
12251234

1226-
If this variable is set to any of the following values, tracebacks printed
1235+
If this variable is set to one of the following values, tracebacks printed
12271236
by the runtime will be annotated with the timestamp of each exception. The
1228-
values control the format of the timestamp. ``us`` or ``1`` prints decimal
1229-
timestamps with microsecond precision, ``ns`` prints the raw timestamp in
1230-
nanoseconds, ``iso`` prints the timestamp formatted by
1231-
:meth:`~datetime.datetime.isoformat` which is also microsecond precision.
1237+
values control the format of the timestamp:
1238+
1239+
* ``us`` or ``1``: Prints decimal timestamps with microsecond precision.
1240+
* ``ns``: Prints the raw timestamp in nanoseconds.
1241+
* ``iso``: Prints the timestamp formatted by :meth:`~datetime.datetime.isoformat` (also microsecond precision).
1242+
* ``0``: Explicitly disables timestamps.
1243+
12321244
The time is not recorded on the :exc:`StopIteration` family of exceptions
12331245
for performance reasons as those are used for control flow rather than
1234-
errors. If unset, empty, or other values this feature remains disabled.
1246+
errors. If unset, empty, or set to invalid values, this feature remains disabled
1247+
when using the environment variable.
1248+
1249+
Note that the command line option :option:`-X` ``traceback_timestamps`` takes
1250+
precedence over this environment variable when both are specified.
12351251

12361252
Formatting of the timestamps only happens at printing time. The ``iso``
12371253
format may be slower due to the complexity of the code involved but is much

Include/cpython/initconfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ typedef struct PyConfig {
149149
int dump_refs;
150150
wchar_t *dump_refs_file;
151151
int malloc_stats;
152+
wchar_t *traceback_timestamps;
152153
wchar_t *filesystem_encoding;
153154
wchar_t *filesystem_errors;
154155
wchar_t *pycache_prefix;

Lib/test/test_capi/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def test_config_get(self):
8585
("stdio_errors", str, None),
8686
("stdlib_dir", str | None, "_stdlib_dir"),
8787
("tracemalloc", int, None),
88+
("traceback_timestamps", str, None),
8889
("use_environment", bool, None),
8990
("use_frozen_modules", bool, None),
9091
("use_hash_seed", bool, None),
@@ -170,6 +171,7 @@ def test_config_get_sys_flags(self):
170171
("warn_default_encoding", "warn_default_encoding", False),
171172
("safe_path", "safe_path", False),
172173
("int_max_str_digits", "int_max_str_digits", False),
174+
("traceback_timestamps", "traceback_timestamps", False),
173175
# "gil" is tested below
174176
):
175177
with self.subTest(flag=flag, name=name, negate=negate):

Lib/test/test_embed.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
584584
'cpu_count': -1,
585585
'faulthandler': False,
586586
'tracemalloc': 0,
587+
'traceback_timestamps': "",
587588
'perf_profiling': 0,
588589
'import_time': False,
589590
'code_debug_ranges': True,

Lib/test/test_sys.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -822,13 +822,22 @@ def test_sys_flags(self):
822822
"warn_default_encoding", "safe_path", "int_max_str_digits")
823823
for attr in attrs:
824824
self.assertTrue(hasattr(sys.flags, attr), attr)
825-
attr_type = bool if attr in ("dev_mode", "safe_path") else int
825+
match attr:
826+
case "dev_mode" | "safe_path":
827+
attr_type = bool
828+
case _:
829+
attr_type = int
826830
self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr)
827831
self.assertTrue(repr(sys.flags))
828832
self.assertEqual(len(sys.flags), len(attrs))
829833

830834
self.assertIn(sys.flags.utf8_mode, {0, 1, 2})
831835

836+
# non-tuple sequence fields
837+
self.assertIsInstance(sys.flags.gil, int)
838+
self.assertIsInstance(sys.flags.traceback_timestamps, str)
839+
840+
832841
def assert_raise_on_new_sys_type(self, sys_attr):
833842
# Users are intentionally prevented from creating new instances of
834843
# sys.flags, sys.version_info, and sys.getwindowsversion.
@@ -1845,11 +1854,9 @@ def test_pythontypes(self):
18451854
# traceback
18461855
if tb is not None:
18471856
check(tb, size('2P2i'))
1848-
# symtable entry
1849-
# XXX
1850-
# sys.flags
1851-
# FIXME: The +1 will not be necessary once gh-122575 is fixed
1852-
check(sys.flags, vsize('') + self.P * (1 + len(sys.flags)))
1857+
# TODO: The non_sequence_fields adjustment is due to GH-122575.
1858+
non_sequence_fields = 2
1859+
check(sys.flags, vsize('') + self.P * (non_sequence_fields + len(sys.flags)))
18531860

18541861
def test_asyncgen_hooks(self):
18551862
old = sys.get_asyncgen_hooks()
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import os
2+
import sys
3+
import unittest
4+
import subprocess
5+
6+
from test.support import script_helper
7+
from test.support.os_helper import TESTFN, unlink
8+
9+
class TracebackTimestampsTests(unittest.TestCase):
10+
def setUp(self):
11+
self.script = """
12+
import sys
13+
import traceback
14+
15+
def cause_exception():
16+
1/0
17+
18+
try:
19+
cause_exception()
20+
except Exception as e:
21+
traceback.print_exc()
22+
"""
23+
self.script_path = TESTFN + '.py'
24+
with open(self.script_path, 'w') as script_file:
25+
script_file.write(self.script)
26+
self.addCleanup(unlink, self.script_path)
27+
28+
# Script to check sys.flags.traceback_timestamps value
29+
self.flags_script = """
30+
import sys
31+
print(repr(sys.flags.traceback_timestamps))
32+
"""
33+
self.flags_script_path = TESTFN + '_flag.py'
34+
with open(self.flags_script_path, 'w') as script_file:
35+
script_file.write(self.flags_script)
36+
self.addCleanup(unlink, self.flags_script_path)
37+
38+
def test_no_traceback_timestamps(self):
39+
"""Test that traceback timestamps are not shown by default"""
40+
result = script_helper.assert_python_ok(self.script_path)
41+
stderr = result.err.decode()
42+
self.assertNotIn("<@", stderr) # No timestamp should be present
43+
44+
def test_traceback_timestamps_env_var(self):
45+
"""Test that PYTHON_TRACEBACK_TIMESTAMPS env var enables timestamps"""
46+
result = script_helper.assert_python_ok(self.script_path, PYTHON_TRACEBACK_TIMESTAMPS="us")
47+
stderr = result.err.decode()
48+
self.assertIn("<@", stderr) # Timestamp should be present
49+
50+
def test_traceback_timestamps_flag_us(self):
51+
"""Test -X traceback_timestamps=us flag"""
52+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=us", self.script_path)
53+
stderr = result.err.decode()
54+
self.assertIn("<@", stderr) # Timestamp should be present
55+
56+
def test_traceback_timestamps_flag_ns(self):
57+
"""Test -X traceback_timestamps=ns flag"""
58+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=ns", self.script_path)
59+
stderr = result.err.decode()
60+
self.assertIn("<@", stderr) # Timestamp should be present
61+
self.assertIn("ns>", stderr) # Should have ns format
62+
63+
def test_traceback_timestamps_flag_iso(self):
64+
"""Test -X traceback_timestamps=iso flag"""
65+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=iso", self.script_path)
66+
stderr = result.err.decode()
67+
self.assertIn("<@", stderr) # Timestamp should be present
68+
self.assertRegex(stderr, r"<@\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}") # ISO format
69+
70+
def test_traceback_timestamps_flag_value(self):
71+
"""Test that sys.flags.traceback_timestamps shows the right value"""
72+
# Default should be empty string
73+
result = script_helper.assert_python_ok(self.flags_script_path)
74+
stdout = result.out.decode().strip()
75+
self.assertEqual(stdout, "''")
76+
77+
# With us flag
78+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=us", self.flags_script_path)
79+
stdout = result.out.decode().strip()
80+
self.assertEqual(stdout, "'us'")
81+
82+
# With ns flag
83+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=ns", self.flags_script_path)
84+
stdout = result.out.decode().strip()
85+
self.assertEqual(stdout, "'ns'")
86+
87+
# With iso flag
88+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=iso", self.flags_script_path)
89+
stdout = result.out.decode().strip()
90+
self.assertEqual(stdout, "'iso'")
91+
92+
def test_traceback_timestamps_env_var_precedence(self):
93+
"""Test that -X flag takes precedence over env var"""
94+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=us",
95+
"-c", "import sys; print(repr(sys.flags.traceback_timestamps))",
96+
PYTHON_TRACEBACK_TIMESTAMPS="ns")
97+
stdout = result.out.decode().strip()
98+
self.assertEqual(stdout, "'us'")
99+
100+
def test_traceback_timestamps_flag_no_value(self):
101+
"""Test -X traceback_timestamps with no value defaults to 'us'"""
102+
result = script_helper.assert_python_ok("-X", "traceback_timestamps", self.flags_script_path)
103+
stdout = result.out.decode().strip()
104+
self.assertEqual(stdout, "'us'")
105+
106+
def test_traceback_timestamps_flag_zero(self):
107+
"""Test -X traceback_timestamps=0 disables the feature"""
108+
# Check that setting to 0 results in empty string in sys.flags
109+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=0", self.flags_script_path)
110+
stdout = result.out.decode().strip()
111+
self.assertEqual(stdout, "''")
112+
113+
# Check that no timestamps appear in traceback
114+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=0", self.script_path)
115+
stderr = result.err.decode()
116+
self.assertNotIn("<@", stderr) # No timestamp should be present
117+
118+
def test_traceback_timestamps_flag_one(self):
119+
"""Test -X traceback_timestamps=1 is equivalent to 'us'"""
120+
result = script_helper.assert_python_ok("-X", "traceback_timestamps=1", self.flags_script_path)
121+
stdout = result.out.decode().strip()
122+
self.assertEqual(stdout, "'us'")
123+
124+
def test_traceback_timestamps_env_var_zero(self):
125+
"""Test PYTHON_TRACEBACK_TIMESTAMPS=0 disables the feature"""
126+
result = script_helper.assert_python_ok(self.flags_script_path, PYTHON_TRACEBACK_TIMESTAMPS="0")
127+
stdout = result.out.decode().strip()
128+
self.assertEqual(stdout, "''")
129+
130+
def test_traceback_timestamps_env_var_one(self):
131+
"""Test PYTHON_TRACEBACK_TIMESTAMPS=1 is equivalent to 'us'"""
132+
result = script_helper.assert_python_ok(self.flags_script_path, PYTHON_TRACEBACK_TIMESTAMPS="1")
133+
stdout = result.out.decode().strip()
134+
self.assertEqual(stdout, "'us'")
135+
136+
def test_traceback_timestamps_invalid_env_var(self):
137+
"""Test that invalid env var values are silently ignored"""
138+
result = script_helper.assert_python_ok(self.flags_script_path, PYTHON_TRACEBACK_TIMESTAMPS="invalid")
139+
stdout = result.out.decode().strip()
140+
self.assertEqual(stdout, "''") # Should default to empty string
141+
142+
def test_traceback_timestamps_invalid_flag(self):
143+
"""Test that invalid flag values cause an error"""
144+
result = script_helper.assert_python_failure("-X", "traceback_timestamps=invalid", self.flags_script_path)
145+
stderr = result.err.decode()
146+
self.assertIn("Invalid value for -X traceback_timestamps option", stderr)
147+
148+
149+
if __name__ == "__main__":
150+
unittest.main()

0 commit comments

Comments
 (0)