From ba55af284cd686e11ba89fb894570035c8d79f70 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Tue, 9 Jun 2026 15:58:36 +0800 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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) From f3cdecc1b70423e6ce9e01735216675451404750 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Fri, 12 Jun 2026 00:21:25 +0800 Subject: [PATCH 5/9] focus on keyword suggestion --- Grammar/python.gram | 6 -- Lib/test/test_traceback.py | 4 ++ Lib/traceback.py | 16 +++-- ...-06-12-00-17-29.gh-issue-151128.-LYO3a.rst | 3 + Parser/parser.c | 68 ------------------- 5 files changed, 16 insertions(+), 81 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst diff --git a/Grammar/python.gram b/Grammar/python.gram index 4bc3e030e520579..9bf3a67939fcf37 100644 --- a/Grammar/python.gram +++ b/Grammar/python.gram @@ -1287,12 +1287,6 @@ 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/test/test_traceback.py b/Lib/test/test_traceback.py index 7dc3364561d8a11..29b830095ffb60c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1815,6 +1815,10 @@ class TestKeywordTypoSuggestions(unittest.TestCase): ("[x for x\nin range(3)\nof x]", "if"), ("[123 fur x\nin range(3)\nif x]", "for"), ("for x im n:\n pass", "in"), + ("switch x:\n case:", "match"), + ("delete x", "del"), + ("function f():", "def"), + ("func f():", "def"), ] def test_keyword_suggestions_from_file(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 8de6bafefec9481..f353eb89ee6fe81 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1493,7 +1493,14 @@ def _find_keyword_typos(self): 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)) + matches.extend( + difflib.get_close_matches( + wrong_name, + keyword.kwlist + keyword.softkwlist, + n=max_matches, + cutoff=0.5 + ) + ) matches = matches[:max_matches] for suggestion in matches: if not suggestion or suggestion == wrong_name: @@ -1799,10 +1806,6 @@ def print(self, *, file=None, chain=True, **kwargs): # function define equivalents 'function': 'def', 'func': 'def', - # null equivalents - 'NULL': 'None', - 'null': 'None', - 'nil': 'None', }) def _substitution_cost(ch_a, ch_b): @@ -1887,8 +1890,7 @@ def _get_cross_language_hint(obj, wrong_name): 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 + return _CROSS_LANGUAGE_KEYWORD_HINTS.get(wrong_name) def _get_safe___dir__(obj): diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst new file mode 100644 index 000000000000000..bf200d3a5120b7c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst @@ -0,0 +1,3 @@ +Cross-language keyword suggestions are now shown for :exc:`SyntaxError` +messages. For example, ``switch x:`` suggests ``match``, ``delete x`` +suggests ``del``, ``function f():`` suggests ``def``. diff --git a/Parser/parser.c b/Parser/parser.c index 42c9070944309c5..c55c081dfc3d8e2 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -22029,8 +22029,6 @@ 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 * @@ -22079,72 +22077,6 @@ 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 2cdd76e4fec5fc9e5acfa4ebd8ae169265258544 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Fri, 12 Jun 2026 00:32:39 +0800 Subject: [PATCH 6/9] remove trailing whitespace --- Lib/traceback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index f353eb89ee6fe81..9dbf1f938e52d9a 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1495,9 +1495,9 @@ def _find_keyword_typos(self): matches.append(suggestion) matches.extend( difflib.get_close_matches( - wrong_name, - keyword.kwlist + keyword.softkwlist, - n=max_matches, + wrong_name, + keyword.kwlist + keyword.softkwlist, + n=max_matches, cutoff=0.5 ) ) From bca2327ed11f2a4808a65dbf908c2f8678453b85 Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Fri, 12 Jun 2026 00:56:39 +0800 Subject: [PATCH 7/9] add test for soft keywords --- Lib/test/test_traceback.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 29b830095ffb60c..aefe64448548299 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1815,6 +1815,11 @@ class TestKeywordTypoSuggestions(unittest.TestCase): ("[x for x\nin range(3)\nof x]", "if"), ("[123 fur x\nin range(3)\nif x]", "for"), ("for x im n:\n pass", "in"), + ("mach x:", "match"), + ("math x:", "match"), + ("match 1:\n cse 1:", "case"), + ("typ x = int", "type"), + ("typed x = int", "type"), ("switch x:\n case:", "match"), ("delete x", "del"), ("function f():", "def"), From 9e13d743180da14584799e7a2d232d9a372f5aaa Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Fri, 12 Jun 2026 11:36:21 +0800 Subject: [PATCH 8/9] add tests for keyword lazy and add void as another crosslang case --- Lib/test/test_traceback.py | 3 +++ Lib/traceback.py | 1 + .../Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index aefe64448548299..26151b7d3b60703 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1820,10 +1820,13 @@ class TestKeywordTypoSuggestions(unittest.TestCase): ("match 1:\n cse 1:", "case"), ("typ x = int", "type"), ("typed x = int", "type"), + ("lazi import x", "lazy"), + ("lezi import x", "lazy"), ("switch x:\n case:", "match"), ("delete x", "del"), ("function f():", "def"), ("func f():", "def"), + ("void f():", "def"), ] def test_keyword_suggestions_from_file(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 9dbf1f938e52d9a..dcdab1f12e9a168 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1806,6 +1806,7 @@ def print(self, *, file=None, chain=True, **kwargs): # function define equivalents 'function': 'def', 'func': 'def', + 'void': 'def', }) def _substitution_cost(ch_a, ch_b): diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst index bf200d3a5120b7c..f34e18322fca19e 100644 --- a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst @@ -1,3 +1,3 @@ -Cross-language keyword suggestions are now shown for :exc:`SyntaxError` -messages. For example, ``switch x:`` suggests ``match``, ``delete x`` -suggests ``del``, ``function f():`` suggests ``def``. +Cross-language keyword suggestions are now shown for :exc:`SyntaxError` messages. +For example, ``switch x:`` suggests ``match``, ``delete x`` suggests ``del``, +``function f():`` suggests ``def``. Contributed by Zang Langyan. From 896dd06a8b3d1753371e9a1c2fdbc5d322df035a Mon Sep 17 00:00:00 2001 From: zang-langyan Date: Fri, 12 Jun 2026 11:39:06 +0800 Subject: [PATCH 9/9] remove whitespace --- .../Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst index f34e18322fca19e..6e760686af2b01f 100644 --- a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst @@ -1,3 +1,3 @@ -Cross-language keyword suggestions are now shown for :exc:`SyntaxError` messages. -For example, ``switch x:`` suggests ``match``, ``delete x`` suggests ``del``, +Cross-language keyword suggestions are now shown for :exc:`SyntaxError` messages. +For example, ``switch x:`` suggests ``match``, ``delete x`` suggests ``del``, ``function f():`` suggests ``def``. Contributed by Zang Langyan.