diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index a3d485c998ac91..2e1e3ba9e12cdd 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1502,6 +1502,50 @@ Traceback (most recent call last): SyntaxError: invalid syntax +Ensure that alternative patterns bind the same names (when there is +a single capture in the first pattern that is not uniformly captured, +the error message is sightly different). + + >>> 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: pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (pattern 1 binds nothing, pattern 2 binds ['x']) + + >>> match 1: + ... case (x, y) | 1: pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (pattern 1 binds ['x', 'y'], pattern 2 binds nothing) + + >>> match 1: + ... case 1 | ("point", {"x": x, "y": y}): pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (pattern 1 binds nothing, pattern 2 binds ['x', 'y']) + + >>> match 1: + ... case ("point", {"x": x, "y": y}) | 1: pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (pattern 1 binds ['x', 'y'], pattern 2 binds nothing) + + >>> match 1: + ... case ("user", {"id": id}) | ("admin", {"name": name}): pass + Traceback (most recent call last): + SyntaxError: alternative patterns bind different names (pattern 1 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 (pattern 1 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..dbdfc9c5d27bb3 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -6270,6 +6270,9 @@ 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; + PyObject *str_nothing = NULL; // the string 'nothing' // We're going to be messing with pc. Keep the original info handy: pattern_context old_pc = *pc; Py_INCREF(pc->stores); @@ -6304,6 +6307,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 +6319,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 +6412,31 @@ 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:; + int control_is_empty = PyList_GET_SIZE(control) == 0; + int pattern_is_empty = ( + mismatched_names == NULL + || PyList_GET_SIZE(mismatched_names) == 0 + ); + + if (control_is_empty || pattern_is_empty) { + str_nothing = PyUnicode_FromString("nothing"); + if (str_nothing == NULL) { + goto error; + } + } + _PyCompile_Error( + c, LOC(p), + "alternative patterns bind different names " + "(pattern 1 binds %S, pattern %zd binds %S)", + control_is_empty ? str_nothing : control, + mismatch_index + 1, + pattern_is_empty ? str_nothing : mismatched_names + ); error: PyMem_Free(old_pc.fail_pop); + Py_XDECREF(mismatched_names); + Py_XDECREF(str_nothing); Py_DECREF(old_pc.stores); Py_XDECREF(control); return ERROR;