From a50593dff7359c349e8f8cabbf1101abb3164fcd Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 3 Nov 2026 13:38:25 -0800 Subject: [PATCH 1/4] gh-161242: Use AC vectorcall for AttributeError, ImportError, NameError These exceptions and their subclasses had no vectorcall, and `__init__` parsed keywords through PyArg_ParseTupleAndKeywords with a temporary empty tuple on every call. Error messages are unchanged except when more keywords than parameters are given, which now names the unexpected keyword. The clinic inputs have no docstring, so docstrings are unchanged and these exceptions don't gain a `__text_signature__`. pyperf microbenchmark (PGO+LTO, run serially): | Benchmark | base | vectorcall | |----------------------------------|:-------:|:---------------------:| | AttributeError(s, name=s, obj=s) | 127 ns | 41.9 ns: 3.02x faster | | ImportError(s, name=s, path=s) | 129 ns | 43.4 ns: 2.96x faster | | NameError(s, name=s) | 88.3 ns | 39.3 ns: 2.24x faster | | ModuleNotFoundError(s, name=s) | 89.9 ns | 40.6 ns: 2.21x faster | | AttributeError(s) | 39.4 ns | 32.1 ns: 1.23x faster | | UnboundLocalError(s) | 38.6 ns | 31.8 ns: 1.22x faster | | NameError(s) | 38.5 ns | 32.2 ns: 1.19x faster | | ImportError(s) | 40.7 ns | 34.4 ns: 1.18x faster | | attribute_error_to_none() | 176 ns | 173 ns: 1.02x faster | Benchmark code: ```python import pyperf runner = pyperf.Runner() setup = """s = 'x' class O: pass o = O() def attribute_error_to_none(): try: o.x except AttributeError: pass """ for stmt in ["AttributeError(s)", "AttributeError(s, name=s, obj=s)", "NameError(s)", "NameError(s, name=s)", "ImportError(s)", "ImportError(s, name=s, path=s)", "ModuleNotFoundError(s, name=s)", "UnboundLocalError(s)", "attribute_error_to_none()"]: runner.timeit(stmt, stmt, setup) ``` Co-Authored-By: Claude Opus 5.5 --- Objects/clinic/exceptions.c.h | 329 +++++++++++++++++++++++++++++++++- Objects/exceptions.c | 110 ++++++------ 2 files changed, 385 insertions(+), 54 deletions(-) diff --git a/Objects/clinic/exceptions.c.h b/Objects/clinic/exceptions.c.h index c06cdb0444ee971..b77606c3c7a8c76 100644 --- a/Objects/clinic/exceptions.c.h +++ b/Objects/clinic/exceptions.c.h @@ -2,8 +2,12 @@ preserve [clinic start generated code]*/ +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +#endif #include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_modsupport.h" // _PyArg_BadArgument() +#include "pycore_runtime.h" // _Py_SINGLETON() static PyObject * BaseException_str_impl(PyBaseExceptionObject *self); @@ -362,6 +366,329 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *matcher_value) return return_value; } + +static int +ImportError_init_impl(PyImportErrorObject *self, PyObject *args, + PyObject *name, PyObject *path, PyObject *name_from); + +static int +ImportError_init_helper(PyObject *self, PyObject *const *args, + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 3 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(name), &_Py_ID(path), &_Py_ID(name_from), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"name", "path", "name_from", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "ImportError", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + PyObject * const *fastargs; + Py_ssize_t noptargs = 0 + nkw - 0; + PyObject *__clinic_args = NULL; + PyObject *name = NULL; + PyObject *path = NULL; + PyObject *name_from = NULL; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (fastargs[0]) { + name = fastargs[0]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (fastargs[1]) { + path = fastargs[1]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + name_from = fastargs[2]; +skip_optional_kwonly: + __clinic_args = PyTuple_FromArray(args, nargs); + if (__clinic_args == NULL) { + goto exit; + } + return_value = ImportError_init_impl((PyImportErrorObject *)self, __clinic_args, name, path, name_from); + +exit: + /* Cleanup for args */ + Py_XDECREF(__clinic_args); + + return return_value; +} + +static int +ImportError_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + return ImportError_init_helper(self, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +static PyObject * +ImportError_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; + int _result; + + assert(_PyType_CAST(type)->tp_init == ImportError_init); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + self = _PyType_CAST(type)->tp_new(_PyType_CAST(type), + (PyObject *)&_Py_SINGLETON(tuple_empty), NULL); + if (self == NULL) { + return NULL; + } + _result = ImportError_init_helper(self, args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + if (_result != 0) { + Py_DECREF(self); + return NULL; + } + return self; +} + +static int +NameError_init_impl(PyNameErrorObject *self, PyObject *args, PyObject *name); + +static int +NameError_init_helper(PyObject *self, PyObject *const *args, + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(name), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"name", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "NameError", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t noptargs = 0 + nkw - 0; + PyObject *__clinic_args = NULL; + PyObject *name = NULL; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + name = fastargs[0]; +skip_optional_kwonly: + __clinic_args = PyTuple_FromArray(args, nargs); + if (__clinic_args == NULL) { + goto exit; + } + return_value = NameError_init_impl((PyNameErrorObject *)self, __clinic_args, name); + +exit: + /* Cleanup for args */ + Py_XDECREF(__clinic_args); + + return return_value; +} + +static int +NameError_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + return NameError_init_helper(self, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +static PyObject * +NameError_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; + int _result; + + assert(_PyType_CAST(type)->tp_init == NameError_init); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + self = _PyType_CAST(type)->tp_new(_PyType_CAST(type), + (PyObject *)&_Py_SINGLETON(tuple_empty), NULL); + if (self == NULL) { + return NULL; + } + _result = NameError_init_helper(self, args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + if (_result != 0) { + Py_DECREF(self); + return NULL; + } + return self; +} + +static int +AttributeError_init_impl(PyAttributeErrorObject *self, PyObject *args, + PyObject *name, PyObject *obj); + +static int +AttributeError_init_helper(PyObject *self, PyObject *const *args, + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(name), &_Py_ID(obj), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"name", "obj", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "AttributeError", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t noptargs = 0 + nkw - 0; + PyObject *__clinic_args = NULL; + PyObject *name = NULL; + PyObject *obj = NULL; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (fastargs[0]) { + name = fastargs[0]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + obj = fastargs[1]; +skip_optional_kwonly: + __clinic_args = PyTuple_FromArray(args, nargs); + if (__clinic_args == NULL) { + goto exit; + } + return_value = AttributeError_init_impl((PyAttributeErrorObject *)self, __clinic_args, name, obj); + +exit: + /* Cleanup for args */ + Py_XDECREF(__clinic_args); + + return return_value; +} + +static int +AttributeError_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + return AttributeError_init_helper(self, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +static PyObject * +AttributeError_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; + int _result; + + assert(_PyType_CAST(type)->tp_init == AttributeError_init); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + self = _PyType_CAST(type)->tp_new(_PyType_CAST(type), + (PyObject *)&_Py_SINGLETON(tuple_empty), NULL); + if (self == NULL) { + return NULL; + } + _result = AttributeError_init_helper(self, args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); + if (_result != 0) { + Py_DECREF(self); + return NULL; + } + return self; +} #define BASEEXCEPTION_ARGS_GETSETDEF {"args", (getter)BaseException_args_get, (setter)BaseException_args_set, NULL}, #define BASEEXCEPTION___TRACEBACK___GETSETDEF {"__traceback__", (getter)BaseException___traceback___get, (setter)BaseException___traceback___set, NULL}, @@ -370,4 +697,4 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *matcher_value) #define BASEEXCEPTION___CAUSE___GETSETDEF {"__cause__", (getter)BaseException___cause___get, (setter)BaseException___cause___set, NULL}, -/*[clinic end generated code: output=20829af0c376b1e9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c8eaffee13bc041d input=a9049054013a1b77]*/ diff --git a/Objects/exceptions.c b/Objects/exceptions.c index e2f757c5ee9c0be..dc0ba0d7e533dda 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -24,8 +24,11 @@ /*[clinic input] class BaseException "PyBaseExceptionObject *" "&PyExc_BaseException" class BaseExceptionGroup "PyBaseExceptionGroupObject *" "&PyExc_BaseExceptionGroup" +class ImportError "PyImportErrorObject *" "&PyExc_ImportError" +class NameError "PyNameErrorObject *" "&PyExc_NameError" +class AttributeError "PyAttributeErrorObject *" "&PyExc_AttributeError" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=b7c45e78cff8edc3]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=fce4682940ab8948]*/ static struct _Py_exc_state* @@ -1778,29 +1781,27 @@ PyImportErrorObject_CAST(PyObject *self) return (PyImportErrorObject *)self; } +/*[clinic input] +@vectorcall +ImportError.__init__ as ImportError_init + + *args: tuple + name: object = NULL + path: object = NULL + name_from: object = NULL + +[clinic start generated code]*/ + static int -ImportError_init(PyObject *op, PyObject *args, PyObject *kwds) +ImportError_init_impl(PyImportErrorObject *self, PyObject *args, + PyObject *name, PyObject *path, PyObject *name_from) +/*[clinic end generated code: output=4917add0f9473bd2 input=4e94a3b530f9be97]*/ { - static char *kwlist[] = {"name", "path", "name_from", 0}; - PyObject *empty_tuple; PyObject *msg = NULL; - PyObject *name = NULL; - PyObject *path = NULL; - PyObject *name_from = NULL; - if (BaseException_init(op, args, NULL) == -1) - return -1; - - PyImportErrorObject *self = PyImportErrorObject_CAST(op); - empty_tuple = PyTuple_New(0); - if (!empty_tuple) - return -1; - if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OOO:ImportError", kwlist, - &name, &path, &name_from)) { - Py_DECREF(empty_tuple); + if (BaseException_init((PyObject *)self, args, NULL) == -1) { return -1; } - Py_DECREF(empty_tuple); Py_XSETREF(self->name, Py_XNewRef(name)); Py_XSETREF(self->path, Py_XNewRef(path)); @@ -1814,6 +1815,7 @@ ImportError_init(PyObject *op, PyObject *args, PyObject *kwds) return 0; } + static int ImportError_clear(PyObject *op) { @@ -2579,33 +2581,29 @@ PyNameErrorObject_CAST(PyObject *self) return (PyNameErrorObject *)self; } -static int -NameError_init(PyObject *op, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"name", NULL}; - PyObject *name = NULL; +/*[clinic input] +@vectorcall +NameError.__init__ as NameError_init - if (BaseException_init(op, args, NULL) == -1) { - return -1; - } + *args: tuple + name: object = NULL - PyObject *empty_tuple = PyTuple_New(0); - if (!empty_tuple) { - return -1; - } - if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$O:NameError", kwlist, - &name)) { - Py_DECREF(empty_tuple); +[clinic start generated code]*/ + +static int +NameError_init_impl(PyNameErrorObject *self, PyObject *args, PyObject *name) +/*[clinic end generated code: output=f15305075d9aa272 input=2e2b7e781ae146b1]*/ +{ + if (BaseException_init((PyObject *)self, args, NULL) == -1) { return -1; } - Py_DECREF(empty_tuple); - PyNameErrorObject *self = PyNameErrorObject_CAST(op); Py_XSETREF(self->name, Py_XNewRef(name)); return 0; } + static int NameError_clear(PyObject *op) { @@ -2662,35 +2660,32 @@ PyAttributeErrorObject_CAST(PyObject *self) return (PyAttributeErrorObject *)self; } -static int -AttributeError_init(PyObject *op, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"name", "obj", NULL}; - PyObject *name = NULL; - PyObject *obj = NULL; +/*[clinic input] +@vectorcall +AttributeError.__init__ as AttributeError_init - if (BaseException_init(op, args, NULL) == -1) { - return -1; - } + *args: tuple + name: object = NULL + obj: object = NULL - PyObject *empty_tuple = PyTuple_New(0); - if (!empty_tuple) { - return -1; - } - if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OO:AttributeError", kwlist, - &name, &obj)) { - Py_DECREF(empty_tuple); +[clinic start generated code]*/ + +static int +AttributeError_init_impl(PyAttributeErrorObject *self, PyObject *args, + PyObject *name, PyObject *obj) +/*[clinic end generated code: output=3d346f9a389f60fa input=bed43aafea47f1a0]*/ +{ + if (BaseException_init((PyObject *)self, args, NULL) == -1) { return -1; } - Py_DECREF(empty_tuple); - PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); Py_XSETREF(self->name, Py_XNewRef(name)); Py_XSETREF(self->obj, Py_XNewRef(obj)); return 0; } + static int AttributeError_clear(PyObject *op) { @@ -4565,6 +4560,15 @@ _PyExc_InitTypes(PyInterpreterState *interp) { exc->tp_vectorcall = BaseException_vectorcall; } + else if (exc->tp_init == ImportError_init) { + exc->tp_vectorcall = ImportError_vectorcall; + } + else if (exc->tp_init == NameError_init) { + exc->tp_vectorcall = NameError_vectorcall; + } + else if (exc->tp_init == AttributeError_init) { + exc->tp_vectorcall = AttributeError_vectorcall; + } } return 0; } From adce6d0d34d155a637ca5ae874167be2e2c4db10 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 5 Nov 2026 23:11:22 -0800 Subject: [PATCH 2/4] Update test_call for the new ImportError keyword error message Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 7115359156eb696..8d8b7f9bbc38fb3 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -208,7 +208,7 @@ def test_varargs14_kw(self): itertools.product, 0, repeat=1, foo=2) def test_varargs15_kw(self): - msg = r"^ImportError\(\) takes at most 3 keyword arguments \(4 given\)$" + msg = r"^ImportError\(\) got an unexpected keyword argument 'foo'$" self.assertRaisesRegex(TypeError, msg, ImportError, 0, name=1, path=2, name_from=3, foo=3) From d73995ef4a0b1aeb94a41a61ef561aed0be3bf71 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Fri, 6 Nov 2026 21:34:04 -0800 Subject: [PATCH 3/4] Add NEWS --- .../2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst new file mode 100644 index 000000000000000..a433d46eaa33923 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst @@ -0,0 +1,3 @@ +Speed up :exc:`AttributeError`, :exc:`ImportError` and +:exc:`NameError` by enabling :ref:`vectorcall`. Unexpected +keyword arguments are now reported by name. \ No newline at end of file From 16aa18f3d7a76d4fd4cead5bba712b838e1a001e Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Fri, 6 Nov 2026 21:37:05 -0800 Subject: [PATCH 4/4] Add missing newline --- .../2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst index a433d46eaa33923..3dbd521612feedb 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-21-21-04.gh-issue-161242.d4vNgC.rst @@ -1,3 +1,3 @@ Speed up :exc:`AttributeError`, :exc:`ImportError` and :exc:`NameError` by enabling :ref:`vectorcall`. Unexpected -keyword arguments are now reported by name. \ No newline at end of file +keyword arguments are now reported by name.