From ba55af284cd686e11ba89fb894570035c8d79f70 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Tue, 9 Jun 2026 15:58:36 +0800 Subject: [PATCH 1/4] gh-151128 Improve SyntaxError message for match --- Lib/traceback.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 614a12f69b32e40..9d6192dcec2a9fc 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1486,7 +1486,7 @@ def _find_keyword_typos(self): max_matches = 3 matches = [] if _suggestions is not None: - suggestion = _suggestions._generate_suggestions(keyword.kwlist, wrong_name) + suggestion = _suggestions._generate_suggestions(keyword.kwlist + keyword.softkwlist + ['switch'], wrong_name) if suggestion: matches.append(suggestion) matches.extend(difflib.get_close_matches(wrong_name, keyword.kwlist, n=max_matches, cutoff=0.5)) @@ -1494,6 +1494,9 @@ def _find_keyword_typos(self): for suggestion in matches: if not suggestion or suggestion == wrong_name: continue + # semantic edge case + if suggestion == 'switch': + suggestion = 'match' # Try to replace the token with the keyword the_lines = error_lines.copy() the_line = the_lines[start[0] - 1][:] From bb0200adbdb983e09e29c9975a25c1726ebbc9e5 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Tue, 9 Jun 2026 16:10:51 +0800 Subject: [PATCH 2/4] suggestion does not compute full match distance --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 9d6192dcec2a9fc..609ea0c1697de41 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1495,7 +1495,7 @@ def _find_keyword_typos(self): if not suggestion or suggestion == wrong_name: continue # semantic edge case - if suggestion == 'switch': + if suggestion == 'switch' or wrong_name == 'switch': suggestion = 'match' # Try to replace the token with the keyword the_lines = error_lines.copy() From 3a2926feb612dbc2bb6be6658e91bb049691f40c Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Tue, 9 Jun 2026 20:59:31 +0800 Subject: [PATCH 3/4] add cross language keyword hints list as a general implementation --- Grammar/python.gram | 6 ++++ Lib/traceback.py | 30 +++++++++++++++++--- Parser/parser.c | 68 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/Grammar/python.gram b/Grammar/python.gram index 9bf3a67939fcf37..4bc3e030e520579 100644 --- a/Grammar/python.gram +++ b/Grammar/python.gram @@ -1287,6 +1287,12 @@ invalid_named_expression(memo): | a=expression ':=' expression { RAISE_SYNTAX_ERROR_KNOWN_LOCATION( a, "cannot use assignment expressions with %s", _PyPegen_get_expr_name(a)) } + | a=expression '&''&' b=expression { + RAISE_SYNTAX_ERROR_KNOWN_RANGE( + a, b, "invalid syntax '&&'. Use 'and' instead.") } + | a=expression '|''|' b=expression { + RAISE_SYNTAX_ERROR_KNOWN_RANGE( + a, b, "invalid syntax '||'. Use 'or' instead.") } | a=NAME '=' b=bitwise_or !('='|':=') { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid syntax. Maybe you meant '==' or ':=' instead of '='?") } | !(list|tuple|genexp|'True'|'None'|'False') a=bitwise_or b='=' bitwise_or !('='|':=') { diff --git a/Lib/traceback.py b/Lib/traceback.py index 609ea0c1697de41..28db2f58bff5e61 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1485,8 +1485,12 @@ def _find_keyword_typos(self): # Limit the number of possible matches to try max_matches = 3 matches = [] + + hint = _get_cross_language_keyword_hint(wrong_name) + if hint: + matches.append(hint) if _suggestions is not None: - suggestion = _suggestions._generate_suggestions(keyword.kwlist + keyword.softkwlist + ['switch'], wrong_name) + suggestion = _suggestions._generate_suggestions(keyword.kwlist + keyword.softkwlist, wrong_name) if suggestion: matches.append(suggestion) matches.extend(difflib.get_close_matches(wrong_name, keyword.kwlist, n=max_matches, cutoff=0.5)) @@ -1494,9 +1498,6 @@ def _find_keyword_typos(self): for suggestion in matches: if not suggestion or suggestion == wrong_name: continue - # semantic edge case - if suggestion == 'switch' or wrong_name == 'switch': - suggestion = 'match' # Try to replace the token with the keyword the_lines = error_lines.copy() the_line = the_lines[start[0] - 1][:] @@ -1790,6 +1791,20 @@ def print(self, *, file=None, chain=True, **kwargs): }) +# Cross-language keyword suggestions. +_CROSS_LANGUAGE_KEYWORD_HINTS = frozendict({ + # C/C++ equivalents + 'switch': 'match', + 'delete': 'del', + # function define equivalents + 'function': 'def', + 'func': 'def', + # null equivalents + 'NULL': 'None', + 'null': 'None', + 'nil': 'None', +}) + def _substitution_cost(ch_a, ch_b): if ch_a == ch_b: return 0 @@ -1869,6 +1884,13 @@ def _get_cross_language_hint(obj, wrong_name): return None +def _get_cross_language_keyword_hint(wrong_name): + """Check if wrong_name is a common keyword from another language + """ + hint = _CROSS_LANGUAGE_KEYWORD_HINTS.get(wrong_name) + return hint + + def _get_safe___dir__(obj): # Use obj.__dir__() to avoid a TypeError when calling dir(obj). # See gh-131001 and gh-139933. diff --git a/Parser/parser.c b/Parser/parser.c index c55c081dfc3d8e2..42c9070944309c5 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -22029,6 +22029,8 @@ invalid_if_expression_rule(Parser *p) // invalid_named_expression: // | expression ':=' expression +// | expression '&' '&' expression +// | expression '|' '|' expression // | NAME '=' bitwise_or !('=' | ':=') // | !(list | tuple | genexp | 'True' | 'None' | 'False') bitwise_or '=' bitwise_or !('=' | ':=') static void * @@ -22077,6 +22079,72 @@ invalid_named_expression_rule(Parser *p) D(fprintf(stderr, "%*c%s invalid_named_expression[%d-%d]: %s failed!\n", p->level, ' ', p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "expression ':=' expression")); } + { // expression '&' '&' expression + if (p->error_indicator) { + p->level--; + return NULL; + } + D(fprintf(stderr, "%*c> invalid_named_expression[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "expression '&' '&' expression")); + Token * _literal; + Token * _literal_1; + expr_ty a; + expr_ty b; + if ( + (a = expression_rule(p)) // expression + && + (_literal = _PyPegen_expect_token(p, 19)) // token='&' + && + (_literal_1 = _PyPegen_expect_token(p, 19)) // token='&' + && + (b = expression_rule(p)) // expression + ) + { + D(fprintf(stderr, "%*c+ invalid_named_expression[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "expression '&' '&' expression")); + _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , b , "invalid syntax '&&'. Use 'and' instead." ); + if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) { + p->error_indicator = 1; + p->level--; + return NULL; + } + goto done; + } + p->mark = _mark; + D(fprintf(stderr, "%*c%s invalid_named_expression[%d-%d]: %s failed!\n", p->level, ' ', + p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "expression '&' '&' expression")); + } + { // expression '|' '|' expression + if (p->error_indicator) { + p->level--; + return NULL; + } + D(fprintf(stderr, "%*c> invalid_named_expression[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "expression '|' '|' expression")); + Token * _literal; + Token * _literal_1; + expr_ty a; + expr_ty b; + if ( + (a = expression_rule(p)) // expression + && + (_literal = _PyPegen_expect_token(p, 18)) // token='|' + && + (_literal_1 = _PyPegen_expect_token(p, 18)) // token='|' + && + (b = expression_rule(p)) // expression + ) + { + D(fprintf(stderr, "%*c+ invalid_named_expression[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "expression '|' '|' expression")); + _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , b , "invalid syntax '||'. Use 'or' instead." ); + if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) { + p->error_indicator = 1; + p->level--; + return NULL; + } + goto done; + } + p->mark = _mark; + D(fprintf(stderr, "%*c%s invalid_named_expression[%d-%d]: %s failed!\n", p->level, ' ', + p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "expression '|' '|' expression")); + } { // NAME '=' bitwise_or !('=' | ':=') if (p->error_indicator) { p->level--; From 27b89b695061c73336e9e24a0ac35ebe72aee43a Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Tue, 9 Jun 2026 21:05:32 +0800 Subject: [PATCH 4/4] trim trailing whitespace --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 28db2f58bff5e61..8de6bafefec9481 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1485,7 +1485,7 @@ def _find_keyword_typos(self): # Limit the number of possible matches to try max_matches = 3 matches = [] - + hint = _get_cross_language_keyword_hint(wrong_name) if hint: matches.append(hint)