diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 0e2fab84aec0ae6..e75376f008dccd8 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -673,6 +673,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(nlocals), "nlocals", 7); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(node_depth), "node_depth", 10); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(node_offset), "node_offset", 11); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(nothing), "nothing", 7); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(ns), "ns", 2); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(nstype), "nstype", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(nt), "nt", 2); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index cbfa80f76b64cd1..467a108e119995e 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -685,6 +685,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(nlocals) STRUCT_FOR_ID(node_depth) STRUCT_FOR_ID(node_offset) + STRUCT_FOR_ID(nothing) STRUCT_FOR_ID(ns) STRUCT_FOR_ID(nstype) STRUCT_FOR_ID(nt) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 34dc6228a0bcb08..17ef3000b512d54 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1960,6 +1960,7 @@ extern "C" { INIT_ID(nlocals), \ INIT_ID(node_depth), \ INIT_ID(node_offset), \ + INIT_ID(nothing), \ INIT_ID(ns), \ INIT_ID(nstype), \ INIT_ID(nt), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index f2f3b9de4a0d87d..5fff1df282cd4af 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -637,6 +637,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_nlocals._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_node_depth._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_node_offset._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_nothing._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_ns._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_nstype._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_nt._ascii.ob_base), diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 39ab53bb1f8b5fe..04c759310d3d956 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 000000000000000..c249c07b212f771 --- /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 840dd3103a81c5a..a975705576a167c 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -6432,6 +6432,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); @@ -6466,6 +6468,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) { @@ -6476,6 +6480,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) { @@ -6567,10 +6573,24 @@ 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 + ); + + _PyCompile_Error( + c, LOC(p), + "alternative patterns bind different names " + "(pattern 1 binds %S, pattern %zd binds %S)", + control_is_empty ? &_Py_ID(nothing) : control, + mismatch_index + 1, + pattern_is_empty ? &_Py_ID(nothing) : mismatched_names + ); error: PyMem_Free(old_pc.fail_pop); + Py_XDECREF(mismatched_names); Py_DECREF(old_pc.stores); Py_XDECREF(control); return ERROR;