Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
686051c
fix(traceback): TracebackException to handle error messages with punc…
krisztian-gajdar Aug 24, 2025
8331ee9
revert unrelated changes
krisztian-gajdar Aug 24, 2025
15c22ab
revert unrelated changes
krisztian-gajdar Aug 24, 2025
f3a91fe
refactor: rename and simplify error suggestion handling
krisztian-gajdar Aug 26, 2025
c22677e
test: add test_exception_punctuation_handling_with_suggestions
krisztian-gajdar Aug 29, 2025
5849913
test: add test_exception_punctuation_handling_with_suggestions
krisztian-gajdar Aug 29, 2025
09b17d2
refactor: review items
krisztian-gajdar Aug 30, 2025
de43049
test: extend error suggestion punctuation handling tests
krisztian-gajdar Aug 30, 2025
28e6e5a
test: new traceback test cases
krisztian-gajdar Aug 30, 2025
aeb7632
refactor traceback tests
krisztian-gajdar Sep 7, 2025
dc0d6b5
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Oct 31, 2025
632f5eb
fix: remove suggestion prompts from exception error messages
krisztian-gajdar Nov 2, 2025
d859b9f
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Nov 2, 2025
fa624e1
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Nov 3, 2025
fc54425
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Dec 16, 2025
0e306a6
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Jan 26, 2026
77e6a19
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Feb 9, 2026
ccf70c1
refactor(traceback): consolidate ModuleNotFoundError handling and fix…
krisztian-gajdar Feb 9, 2026
05f4932
Merge branch 'main' into traceback-to-handle-error-msgs-with-period
krisztian-gajdar Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3866,6 +3866,50 @@ def test_traceback_header(self):
exc = traceback.TracebackException(Exception, Exception("haven"), None)
self.assertEqual(list(exc.format()), ["Exception: haven\n"])

def test_name_error_punctuation_with_suggestions(self):
def format_error(message, name):
return self.format_error(NameError, message, name=name)

test_cases = [
("a.", "time", "NameError: a. Did you forget to import 'time'?\n"),
("b?", "time", "NameError: b? Did you forget to import 'time'?\n"),
("c!", "time", "NameError: c! Did you forget to import 'time'?\n"),
("d", "time", "NameError: d. Did you forget to import 'time'?\n"),
("e", "foo123", "NameError: e\n"),
]
for message, name, expected in test_cases:
with self.subTest(message=message):
message = format_error(message, name)
self.assertEqual(message, expected)

with self.subTest("stdlib module import suggestion"):
message = format_error("foo", "abc")
expected_message = (
"NameError: foo. Did you mean: 'abs'? "
"Or did you forget to import 'abc'?\n"
)
self.assertEqual(message, expected_message)

with self.subTest("'did you mean' suggestion"):
message = format_error("bar", "flaot")
expected_message = "NameError: bar. Did you mean: 'float'?\n"
self.assertEqual(message, expected_message)

def test_import_error_punctuation_handling_with_suggestions(self):
def format_error(message):
return self.format_error(ImportError, message, name="math", name_from="sinq")

test_cases = [
("a.", "ImportError: a. Did you mean: 'sin'?\n"),
("b?", "ImportError: b? Did you mean: 'sin'?\n"),
("c!", "ImportError: c! Did you mean: 'sin'?\n"),
("d", "ImportError: d. Did you mean: 'sin'?\n"),
]
for message, expected in test_cases:
with self.subTest(message=message):
message = format_error(message)
self.assertEqual(message, expected)

@requires_debug_ranges()
def test_print(self):
def f():
Expand Down Expand Up @@ -3903,6 +3947,14 @@ def test_dont_swallow_cause_or_context_of_falsey_exception(self):
except FalseyException as e:
self.assertIn(context_message, traceback.format_exception(e))

@staticmethod
def format_error(exc_type, exc_message, *args, **kwargs):
try:
raise exc_type(exc_message, *args, **kwargs)
except exc_type as e:
exc = traceback.TracebackException.from_exception(e)
return list(exc.format())[-1]


class TestTracebackException_ExceptionGroups(unittest.TestCase):
def setUp(self):
Expand Down
115 changes: 64 additions & 51 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,63 +1086,40 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,

self._is_syntax_error = False
self._have_exc_type = exc_type is not None
if exc_type is not None:

if self._have_exc_type:
self.exc_type_qualname = exc_type.__qualname__
self.exc_type_module = exc_type.__module__
else:
self.exc_type_qualname = None
self.exc_type_module = None

if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
self.filename = exc_value.filename
lno = exc_value.lineno
self.lineno = str(lno) if lno is not None else None
end_lno = exc_value.end_lineno
self.end_lineno = str(end_lno) if end_lno is not None else None
self.text = exc_value.text
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
self._is_syntax_error = True
self._exc_metadata = getattr(exc_value, "_metadata", None)
elif exc_type and issubclass(exc_type, ImportError) and \
getattr(exc_value, "name_from", None) is not None:
wrong_name = getattr(exc_value, "name_from", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
if suggestion.isascii():
self._str += f". Did you mean: '{suggestion}'?"
else:
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
elif exc_type and issubclass(exc_type, ModuleNotFoundError):
module_name = getattr(exc_value, "name", None)
if module_name in sys.stdlib_module_names:
message = _MISSING_STDLIB_MODULE_MESSAGES.get(
if issubclass(exc_type, SyntaxError):
# Handle SyntaxErrors specially
self.filename = exc_value.filename
lno = exc_value.lineno
self.lineno = str(lno) if lno is not None else None
end_lno = exc_value.end_lineno
self.end_lineno = str(end_lno) if end_lno is not None else None
self.text = exc_value.text
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
self._is_syntax_error = True
self._exc_metadata = getattr(exc_value, "_metadata", None)
elif (issubclass(exc_type, ModuleNotFoundError)
and getattr(exc_value, "name", None) in sys.stdlib_module_names):
module_name = exc_value.name
self._str = _MISSING_STDLIB_MODULE_MESSAGES.get(
module_name,
f"Standard library module {module_name!r} was not found"
)
self._str = message
elif sys.flags.no_site:
self._str += (". Site initialization is disabled, did you forget to "
+ "add the site-packages directory to sys.path "
+ "or to enable your virtual environment?")
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
if suggestion.isascii():
self._str += f". Did you mean: '{suggestion}'?"
elif suggestion := _suggestion_message(exc_type, exc_value, exc_traceback):
if self._str.endswith(('.', '?', '!')):
punctuation = ''
else:
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
if issubclass(exc_type, NameError):
wrong_name = getattr(exc_value, "name", None)
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
punctuation = '.'
self._str += f"{punctuation} {suggestion}"
else:
self.exc_type_qualname = None
self.exc_type_module = None

if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
Expand Down Expand Up @@ -1771,6 +1748,42 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
return suggestion


def _suggestion_message(exc_type, exc_value, exc_traceback):
if (
issubclass(exc_type, ModuleNotFoundError)
and sys.flags.no_site
and getattr(exc_value, "name", None) not in sys.stdlib_module_names
):
return ("Site initialization is disabled, did you forget to "
"add the site-packages directory to sys.path "
"or to enable your virtual environment?")
if issubclass(exc_type, (ImportError, NameError, AttributeError)):
if issubclass(exc_type, ImportError):
wrong_name = getattr(exc_value, "name_from", None)
else:
wrong_name = getattr(exc_value, "name", None)
if wrong_name:
other_name = _compute_suggestion_error(
exc_value, exc_traceback, wrong_name
)
maybe_stdlib_import = (
issubclass(exc_type, NameError)
and wrong_name in sys.stdlib_module_names
)
if not other_name:
if maybe_stdlib_import:
return f"Did you forget to import '{wrong_name}'?"
return None
if other_name.isascii():
text = f"Did you mean: '{other_name}'?"
else:
text = f"Did you mean: '{other_name}' ({other_name!a})?"
if maybe_stdlib_import:
return f"{text} Or did you forget to import '{wrong_name}'?"
return text
return None


def _levenshtein_distance(a, b, max_cost):
# A Python implementation of Python/suggestions.c:levenshtein_distance.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid double punctuation in :class:`~traceback.TracebackException` messages
Loading