From b2194ac8ee9a18771d8527fdd36ec596a2da8482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:22:13 +0100 Subject: [PATCH 1/3] gh-145019: improve `SyntaxError` when `match` patterns bind different names --- Lib/test/test_syntax.py | 27 +++++++++++++++++++ ...-03-14-11-20-52.gh-issue-145019.ls2EXo.rst | 2 ++ Python/codegen.c | 26 ++++++++++++++++-- 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-14-11-20-52.gh-issue-145019.ls2EXo.rst diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index a3d485c998ac91..612a30e7457e44 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1502,6 +1502,33 @@ Traceback (most recent call last): SyntaxError: invalid syntax +Ensure that alternative patterns bind the same names + + >>> match 1: + ... case x | 1: pass + Traceback (most recent call last): + SyntaxError: name capture 'x' makes remaining patterns unreachable + + >>> match 1: + ... case x | y: pass + Traceback (most recent call last): + SyntaxError: name capture 'x' makes remaining patterns unreachable + + >>> match 1: + ... case 1 | x: ... + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (first pattern binds no names, pattern 2 binds ['x']) + + >>> match 1: + ... case ("user", {"id": id}) | ("admin", {"name": name}): pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 2 binds ['name']) + + >>> match 1: + ... case ("user", {"id": id}) | ("admin", {"id": id}) | ("other", {"ip": ip}): pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (first pattern binds ['id'], pattern 3 binds ['ip']) + Incomplete dictionary literals >>> {1:2, 3:4, 5} diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-14-11-20-52.gh-issue-145019.ls2EXo.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-14-11-20-52.gh-issue-145019.ls2EXo.rst new file mode 100644 index 00000000000000..c249c07b212f77 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-14-11-20-52.gh-issue-145019.ls2EXo.rst @@ -0,0 +1,2 @@ +Improve :exc:`SyntaxError` when :keyword:`match` alternative patterns bind +different names. Patch by Bénédikt Tran. diff --git a/Python/codegen.c b/Python/codegen.c index 5749b615386717..f9786c47f94d9c 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -6270,6 +6270,8 @@ codegen_pattern_or(compiler *c, pattern_ty p, pattern_context *pc) NEW_JUMP_TARGET_LABEL(c, end); Py_ssize_t size = asdl_seq_LEN(p->v.MatchOr.patterns); assert(size > 1); + PyObject *mismatched_names = NULL; + Py_ssize_t mismatch_index = 0; // We're going to be messing with pc. Keep the original info handy: pattern_context old_pc = *pc; Py_INCREF(pc->stores); @@ -6304,6 +6306,8 @@ codegen_pattern_or(compiler *c, pattern_ty p, pattern_context *pc) control = Py_NewRef(pc->stores); } else if (nstores != PyList_GET_SIZE(control)) { + mismatch_index = i; + mismatched_names = Py_NewRef(pc->stores); goto diff; } else if (nstores) { @@ -6314,6 +6318,8 @@ codegen_pattern_or(compiler *c, pattern_ty p, pattern_context *pc) Py_ssize_t istores = PySequence_Index(pc->stores, name); if (istores < 0) { PyErr_Clear(); + mismatch_index = i; + mismatched_names = Py_NewRef(pc->stores); goto diff; } if (icontrol != istores) { @@ -6405,10 +6411,26 @@ codegen_pattern_or(compiler *c, pattern_ty p, pattern_context *pc) // Pop the copy of the subject: ADDOP(c, LOC(p), POP_TOP); return SUCCESS; -diff: - _PyCompile_Error(c, LOC(p), "alternative patterns bind different names"); +diff:; + PyObject *no_names = NULL; + if (PyList_GET_SIZE(control) == 0 || !mismatched_names) { + no_names = PyUnicode_FromString("no names"); + if (no_names == NULL) { + goto error; + } + } + _PyCompile_Error( + c, LOC(p), + "alternative patterns bind different names " + "(first pattern binds %S, pattern %d binds %S)", + PyList_GET_SIZE(control) == 0 ? no_names : control, + mismatch_index + 1, + mismatched_names == NULL ? no_names : mismatched_names + ); + Py_XDECREF(no_names); error: PyMem_Free(old_pc.fail_pop); + Py_XDECREF(mismatched_names); Py_DECREF(old_pc.stores); Py_XDECREF(control); return ERROR; From 37d5b5cdd4d547ae0dd0ea1f6a3326bf6b5b75b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:29:53 +0100 Subject: [PATCH 2/3] Update Lib/test/test_syntax.py --- Lib/test/test_syntax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 612a30e7457e44..bf90750abfc305 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1515,7 +1515,7 @@ SyntaxError: name capture 'x' makes remaining patterns unreachable >>> match 1: - ... case 1 | x: ... + ... case 1 | x: pass Traceback (most recent call last): SyntaxError: alternative patterns bind different names (first pattern binds no names, pattern 2 binds ['x']) From 5cc7b11e9161c0d07f3a935c1bb6c5c73667eccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:30:37 +0100 Subject: [PATCH 3/3] use correct format specified `%d` -> `%zd` Co-authored-by: AN Long --- Python/codegen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/codegen.c b/Python/codegen.c index f9786c47f94d9c..871b486fc0cf84 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -6422,7 +6422,7 @@ diff:; _PyCompile_Error( c, LOC(p), "alternative patterns bind different names " - "(first pattern binds %S, pattern %d binds %S)", + "(first pattern binds %S, pattern %zd binds %S)", PyList_GET_SIZE(control) == 0 ? no_names : control, mismatch_index + 1, mismatched_names == NULL ? no_names : mismatched_names