]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145876: Do not mask KeyErrors raised during dictionary unpacking in call (GH-146472)
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 29 Mar 2026 08:58:52 +0000 (11:58 +0300)
committerGitHub <noreply@github.com>
Sun, 29 Mar 2026 08:58:52 +0000 (11:58 +0300)
KeyErrors raised in keys() or __getitem__() during dictionary unpacking
in call (func(**mymapping)) are no longer masked by TypeError.

Include/internal/pycore_ceval.h
Include/internal/pycore_dict.h
Lib/test/test_extcall.py
Lib/test/test_unpack_ex.py
Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst
Modules/_testinternalcapi/test_cases.c.h
Objects/dictobject.c
Python/bytecodes.c
Python/ceval.c
Python/executor_cases.c.h
Python/generated_cases.c.h

index 2c83101b6b26feeb10726ac717fc7b906bb60232..9fd3be744049078d0ce92c3eced9e1beee9a8764 100644 (file)
@@ -302,7 +302,7 @@ PyAPI_FUNC(int) _PyEval_ExceptionGroupMatch(_PyInterpreterFrame *, PyObject* exc
 PyAPI_FUNC(void) _PyEval_FormatAwaitableError(PyThreadState *tstate, PyTypeObject *type, int oparg);
 PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc, const char *format_str, PyObject *obj);
 PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
-PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs);
+PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey);
 PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *);
 
 PyAPI_FUNC(PyObject *) _PyEval_LazyImportName(
index 6d7d68eda84c5afdc64dd21d2e3bd5e9530f9bee..a1d1e10ec5c022d467a0fc3e32ac5e6461d485c1 100644 (file)
@@ -55,7 +55,7 @@ extern Py_ssize_t _PyDict_SizeOf_LockHeld(PyDictObject *);
    of a key wins, if override is 2, a KeyError with conflicting key as
    argument is raised.
 */
-PyAPI_FUNC(int) _PyDict_MergeEx(PyObject *mp, PyObject *other, int override);
+PyAPI_FUNC(int) _PyDict_MergeUniq(PyObject *mp, PyObject *other, PyObject **dupkey);
 
 extern void _PyDict_DebugMallocStats(FILE *out);
 
index 20dd16dd05d12d30fe8dd2d0296ec346664c120f..4da5601e80295f53707264cfcda0de8e03ef0218 100644 (file)
@@ -634,11 +634,11 @@ def test_errors_in_keys():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> f(**D())
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 def test_errors_in_keys_next():
@@ -666,11 +666,11 @@ def test_errors_in_keys_next():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> f(**D())
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 def test_errors_in_getitem():
@@ -694,11 +694,11 @@ def test_errors_in_getitem():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> f(**D())
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 import doctest
index d3ba8133c41d5716061fecda7ab6c275b2568a69..33c96b84964b591ced7b1d0d8a89dc7a8f136ac9 100644 (file)
@@ -681,11 +681,11 @@ def test_errors_in_keys():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> {**D()}
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 def test_errors_in_keys_next():
@@ -712,11 +712,11 @@ def test_errors_in_keys_next():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> {**D()}
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 def test_errors_in_getitem():
@@ -739,11 +739,11 @@ def test_errors_in_getitem():
       ...
     AttributeError: some error
 
-    >>> exc = TypeError('some error')
+    >>> exc = KeyError('some error')
     >>> {**D()}
     Traceback (most recent call last):
       ...
-    TypeError: some error
+    KeyError: 'some error'
     """
 
 __test__ = {'doctests' : doctests}
index 7923d80953fd53c7af6aca8287e21a55288d499d..86579634fa14ce4e8e2227f5cc8faee2f0389c35 100644 (file)
@@ -1,3 +1,3 @@
-:exc:`AttributeError`\ s raised in :meth:`!keys` or :meth:`!__getitem__`
+:exc:`AttributeError`\ s and :exc:`KeyError`\ s raised in :meth:`!keys` or :meth:`!__getitem__`
 during dictionary unpacking (``{**mymapping}`` or ``func(**mymapping)``) are
 no longer masked by :exc:`TypeError`.
index 833432978c1561a7dccd41df6ba69400f417a620..a3c840980edeaa00aa9ff0ad85650a287a9846ca 100644 (file)
                 PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable);
                 PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
                 PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
+                PyObject *dupkey = NULL;
                 _PyFrame_SetStackPointer(frame, stack_pointer);
-                int err = _PyDict_MergeEx(dict_o, update_o, 2);
+                int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey);
                 stack_pointer = _PyFrame_GetStackPointer(frame);
                 if (err < 0) {
                     _PyFrame_SetStackPointer(frame, stack_pointer);
-                    _PyEval_FormatKwargsError(tstate, callable_o, update_o);
+                    _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey);
+                    Py_XDECREF(dupkey);
                     stack_pointer = _PyFrame_GetStackPointer(frame);
                     JUMP_TO_LABEL(error);
                 }
index a6c40454d52a711e81885922695a0b155735f43e..e392e44267e2875020a43e3b016dd4ff69a23462 100644 (file)
@@ -139,7 +139,7 @@ As a consequence of this, split keys have a maximum size of 16.
 static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
                                 PyObject *kwds);
 static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
-static int dict_merge(PyObject *a, PyObject *b, int override);
+static int dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey);
 static int dict_contains(PyObject *op, PyObject *key);
 static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override);
 
@@ -3391,7 +3391,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)
             Py_DECREF(d);
             return NULL;
         }
-        if (dict_merge(copy, d, 1) < 0) {
+        if (dict_merge(copy, d, 1, NULL) < 0) {
             Py_DECREF(d);
             Py_DECREF(copy);
             return NULL;
@@ -3887,14 +3887,14 @@ static int
 dict_update_arg(PyObject *self, PyObject *arg)
 {
     if (PyAnyDict_CheckExact(arg)) {
-        return dict_merge(self, arg, 1);
+        return dict_merge(self, arg, 1, NULL);
     }
     int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys));
     if (has_keys < 0) {
         return -1;
     }
     if (has_keys) {
-        return dict_merge(self, arg, 1);
+        return dict_merge(self, arg, 1, NULL);
     }
     return dict_merge_from_seq2(self, arg, 1);
 }
@@ -3915,7 +3915,7 @@ dict_update_common(PyObject *self, PyObject *args, PyObject *kwds,
 
     if (result == 0 && kwds != NULL) {
         if (PyArg_ValidateKeywordArguments(kwds))
-            result = dict_merge(self, kwds, 1);
+            result = dict_merge(self, kwds, 1, NULL);
         else
             result = -1;
     }
@@ -4059,7 +4059,7 @@ PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override)
 }
 
 static int
-dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
+dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override, PyObject **dupkey)
 {
     assert(can_modify_dict(mp));
     ASSERT_DICT_LOCKED(other);
@@ -4068,10 +4068,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
         /* a.update(a) or a.update({}); nothing to do */
         return 0;
     if (mp->ma_used == 0) {
-        /* Since the target dict is empty, PyDict_GetItem()
-            * always returns NULL.  Setting override to 1
-            * skips the unnecessary test.
-            */
+        /* Since the target dict is empty, _PyDict_Contains_KnownHash()
+         * always returns 0.  Setting override to 1
+         * skips the unnecessary test.
+         */
         override = 1;
         PyDictKeysObject *okeys = other->ma_keys;
 
@@ -4131,11 +4131,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
                 err = insertdict(mp, Py_NewRef(key), hash, Py_NewRef(value));
             }
             else if (err > 0) {
-                if (override != 0) {
-                    _PyErr_SetKeyError(key);
+                if (dupkey != NULL) {
+                    *dupkey = key;
                     Py_DECREF(value);
-                    Py_DECREF(key);
-                    return -1;
+                    return -2;
                 }
                 err = 0;
             }
@@ -4155,7 +4154,7 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
 }
 
 static int
-dict_merge(PyObject *a, PyObject *b, int override)
+dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey)
 {
     assert(a != NULL);
     assert(b != NULL);
@@ -4167,7 +4166,7 @@ dict_merge(PyObject *a, PyObject *b, int override)
         PyDictObject *other = (PyDictObject*)b;
         int res;
         Py_BEGIN_CRITICAL_SECTION2(a, b);
-        res = dict_dict_merge((PyDictObject *)a, other, override);
+        res = dict_dict_merge((PyDictObject *)a, other, override, dupkey);
         ASSERT_CONSISTENT(a);
         Py_END_CRITICAL_SECTION2();
         return res;
@@ -4202,15 +4201,18 @@ dict_merge(PyObject *a, PyObject *b, int override)
                 status = dict_contains(a, key);
                 if (status != 0) {
                     if (status > 0) {
-                        if (override == 0) {
+                        if (dupkey == NULL) {
                             Py_DECREF(key);
                             continue;
                         }
-                        _PyErr_SetKeyError(key);
+                        *dupkey = key;
+                        res = -2;
+                    }
+                    else {
+                        Py_DECREF(key);
+                        res = -1;
                     }
-                    Py_DECREF(key);
                     Py_DECREF(iter);
-                    res = -1;
                     goto slow_exit;
                 }
             }
@@ -4246,7 +4248,7 @@ slow_exit:
 }
 
 static int
-dict_merge_api(PyObject *a, PyObject *b, int override)
+dict_merge_api(PyObject *a, PyObject *b, int override, PyObject **dupkey)
 {
     /* We accept for the argument either a concrete dictionary object,
      * or an abstract "mapping" object.  For the former, we can do
@@ -4262,26 +4264,26 @@ dict_merge_api(PyObject *a, PyObject *b, int override)
         }
         return -1;
     }
-    return dict_merge(a, b, override);
+    return dict_merge(a, b, override, dupkey);
 }
 
 int
 PyDict_Update(PyObject *a, PyObject *b)
 {
-    return dict_merge_api(a, b, 1);
+    return dict_merge_api(a, b, 1, NULL);
 }
 
 int
 PyDict_Merge(PyObject *a, PyObject *b, int override)
 {
     /* XXX Deprecate override not in (0, 1). */
-    return dict_merge_api(a, b, override != 0);
+    return dict_merge_api(a, b, override != 0, NULL);
 }
 
 int
-_PyDict_MergeEx(PyObject *a, PyObject *b, int override)
+_PyDict_MergeUniq(PyObject *a, PyObject *b, PyObject **dupkey)
 {
-    return dict_merge_api(a, b, override);
+    return dict_merge_api(a, b, 2, dupkey);
 }
 
 /*[clinic input]
@@ -4421,7 +4423,7 @@ copy_lock_held(PyObject *o, int as_frozendict)
     }
     if (copy == NULL)
         return NULL;
-    if (dict_merge(copy, o, 1) == 0)
+    if (dict_merge(copy, o, 1, NULL) == 0)
         return copy;
     Py_DECREF(copy);
     return NULL;
index a6719a660e39c0aa8a7cc1353b369641d9c20061..ab128661096ea1e4e5c7b03290bc92276a64cd1d 100644 (file)
@@ -2416,10 +2416,12 @@ dummy_func(
             PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable);
             PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
             PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
+            PyObject *dupkey = NULL;
 
-            int err = _PyDict_MergeEx(dict_o, update_o, 2);
+            int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey);
             if (err < 0) {
-                _PyEval_FormatKwargsError(tstate, callable_o, update_o);
+                _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey);
+                Py_XDECREF(dupkey);
                 ERROR_NO_POP();
             }
             u = update;
index f95900ae01a6afa023ffa922d6a99434813f18f9..49aeffc3caf849e0a091f9edbec6296b20337a1d 100644 (file)
@@ -3456,9 +3456,18 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args)
 }
 
 void
-_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
-{
-    /* _PyDict_MergeEx raises attribute
+_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey)
+{
+    if (dupkey != NULL) {
+        PyObject *funcstr = _PyObject_FunctionStr(func);
+        _PyErr_Format(
+            tstate, PyExc_TypeError,
+            "%V got multiple values for keyword argument '%S'",
+            funcstr, "finction", dupkey);
+        Py_XDECREF(funcstr);
+        return;
+    }
+    /* _PyDict_MergeUniq raises attribute
      * error (percolated from an attempt
      * to get 'keys' attribute) instead of
      * a type error if its second argument
@@ -3478,27 +3487,6 @@ _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwarg
             _PyErr_ChainExceptions1Tstate(tstate, exc);
         }
     }
-    else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
-        PyObject *exc = _PyErr_GetRaisedException(tstate);
-        PyObject *args = PyException_GetArgs(exc);
-        if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) {
-            _PyErr_Clear(tstate);
-            PyObject *funcstr = _PyObject_FunctionStr(func);
-            if (funcstr != NULL) {
-                PyObject *key = PyTuple_GET_ITEM(args, 0);
-                _PyErr_Format(
-                    tstate, PyExc_TypeError,
-                    "%U got multiple values for keyword argument '%S'",
-                    funcstr, key);
-                Py_DECREF(funcstr);
-            }
-            Py_XDECREF(exc);
-        }
-        else {
-            _PyErr_SetRaisedException(tstate, exc);
-        }
-        Py_DECREF(args);
-    }
 }
 
 void
index 6f08adf38f13be78d50002e1cafa160cddcb1a86..93533e374fdf446a0255f4c377774d19b9b7bdbc 100644 (file)
             PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable);
             PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
             PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
+            PyObject *dupkey = NULL;
             stack_pointer[0] = update;
             stack_pointer += 1;
             ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            int err = _PyDict_MergeEx(dict_o, update_o, 2);
+            int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey);
             stack_pointer = _PyFrame_GetStackPointer(frame);
             if (err < 0) {
                 _PyFrame_SetStackPointer(frame, stack_pointer);
-                _PyEval_FormatKwargsError(tstate, callable_o, update_o);
+                _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey);
+                Py_XDECREF(dupkey);
                 stack_pointer = _PyFrame_GetStackPointer(frame);
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
index d2c958f39c7ee41e7ba9ca93406e43dc79bc6707..4bec8db14190f993b3877bcdef07d36cf0dedc50 100644 (file)
                 PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable);
                 PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
                 PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
+                PyObject *dupkey = NULL;
                 _PyFrame_SetStackPointer(frame, stack_pointer);
-                int err = _PyDict_MergeEx(dict_o, update_o, 2);
+                int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey);
                 stack_pointer = _PyFrame_GetStackPointer(frame);
                 if (err < 0) {
                     _PyFrame_SetStackPointer(frame, stack_pointer);
-                    _PyEval_FormatKwargsError(tstate, callable_o, update_o);
+                    _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey);
+                    Py_XDECREF(dupkey);
                     stack_pointer = _PyFrame_GetStackPointer(frame);
                     JUMP_TO_LABEL(error);
                 }