gh-155051: Reject None keyword arguments in decimal.localcontext() (GH-155054)

serhiy-storchaka <[email protected]> Sun, 02 Aug 2026 03:09:12 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/96ebb20fc2f0542d9387091e49626f9a1132de82
commit: 96ebb20fc2f0542d9387091e49626f9a1132de82
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-02T07:08:59Z
summary:

gh-155051: Reject None keyword arguments in decimal.localcontext() (GH-155054)

None is not a valid value for any of the context attributes.  The C
implementation silently ignored it, because it shared the code with the
Context constructor, where None means "not specified".  The pure Python
implementation always rejected it.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst
M Lib/test/test_decimal.py
M Modules/_decimal/_decimal.c
M Modules/_decimal/clinic/_decimal.c.h

diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index 493732c22e8231..b8c09c7f43e3e3 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -3798,6 +3798,13 @@ def test_localcontext_kwargs(self):
         self.assertRaises(TypeError, self.decimal.localcontext, Emin="")
         self.assertRaises(TypeError, self.decimal.localcontext, Emax="")
 
+        # None is not a valid value for any of these attributes.
+        for name in ('prec', 'rounding', 'Emin', 'Emax', 'capitals', 'clamp',
+                     'flags', 'traps'):
+            with self.subTest(name=name):
+                self.assertRaises(TypeError, self.decimal.localcontext,
+                                  **{name: None})
+
     def test_local_context_kwargs_does_not_overwrite_existing_argument(self):
         ctx = self.decimal.getcontext()
         orig_prec = ctx.prec
diff --git a/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst b/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst
new file mode 100644
index 00000000000000..d2be40f243d296
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst
@@ -0,0 +1,3 @@
+:func:`decimal.localcontext` now raises :exc:`TypeError` if a keyword argument
+is ``None``, as the pure Python implementation already did.  Previously the C
+implementation silently ignored it.
diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c
index 38944e6a9d9017..1244505db7c612 100644
--- a/Modules/_decimal/_decimal.c
+++ b/Modules/_decimal/_decimal.c
@@ -1333,7 +1333,7 @@ context_setattr(PyObject *self, PyObject *name, PyObject *value)
     return PyObject_GenericSetAttr(self, name, value);
 }
 
-/* In the constructor and in localcontext() None means "not specified". */
+/* In the constructor None means "not specified". */
 #define NONE_TO_NULL(x) ((x) == Py_None ? NULL : (x))
 
 /* Set the given attributes.  An attribute is left unchanged if the
@@ -2099,14 +2099,14 @@ _decimal.localcontext
 
     ctx as local: object = None
     *
-    prec: object = None
-    rounding: object = None
-    Emin: object = None
-    Emax: object = None
-    capitals: object = None
-    clamp: object = None
-    flags: object = None
-    traps: object = None
+    prec: object = NULL
+    rounding: object = NULL
+    Emin: object = NULL
+    Emax: object = NULL
+    capitals: object = NULL
+    clamp: object = NULL
+    flags: object = NULL
+    traps: object = NULL
 
 Return a context manager for a copy of the supplied context.
 
@@ -2121,7 +2121,7 @@ _decimal_localcontext_impl(PyObject *module, PyObject *local, PyObject *prec,
                            PyObject *rounding, PyObject *Emin,
                            PyObject *Emax, PyObject *capitals,
                            PyObject *clamp, PyObject *flags, PyObject *traps)
-/*[clinic end generated code: output=9bf4e47742a809b0 input=490307b9689c3856]*/
+/*[clinic end generated code: output=9bf4e47742a809b0 input=616abb6ee1654373]*/
 {
     PyObject *global;
 
@@ -2142,9 +2142,9 @@ _decimal_localcontext_impl(PyObject *module, PyObject *local, PyObject *prec,
     }
 
     int ret = context_setattrs(
-        local_copy, NONE_TO_NULL(prec), NONE_TO_NULL(rounding),
-        NONE_TO_NULL(Emin), NONE_TO_NULL(Emax), NONE_TO_NULL(capitals),
-        NONE_TO_NULL(clamp), NONE_TO_NULL(flags), NONE_TO_NULL(traps)
+        local_copy, prec, rounding,
+        Emin, Emax, capitals,
+        clamp, flags, traps
     );
     if (ret < 0) {
         Py_DECREF(local_copy);
diff --git a/Modules/_decimal/clinic/_decimal.c.h b/Modules/_decimal/clinic/_decimal.c.h
index 7c45bc66569bb5..965e5f8d0da995 100644
--- a/Modules/_decimal/clinic/_decimal.c.h
+++ b/Modules/_decimal/clinic/_decimal.c.h
@@ -628,14 +628,14 @@ _decimal_localcontext(PyObject *module, PyObject *const *args, Py_ssize_t nargs,
     PyObject *argsbuf[9];
     Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
     PyObject *local = Py_None;
-    PyObject *prec = Py_None;
-    PyObject *rounding = Py_None;
-    PyObject *Emin = Py_None;
-    PyObject *Emax = Py_None;
-    PyObject *capitals = Py_None;
-    PyObject *clamp = Py_None;
-    PyObject *flags = Py_None;
-    PyObject *traps = Py_None;
+    PyObject *prec = NULL;
+    PyObject *rounding = NULL;
+    PyObject *Emin = NULL;
+    PyObject *Emax = NULL;
+    PyObject *capitals = NULL;
+    PyObject *clamp = NULL;
+    PyObject *flags = NULL;
+    PyObject *traps = NULL;
 
     args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
             /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -7100,4 +7100,4 @@ _decimal_Context_same_quantum(PyObject *context, PyTypeObject *cls, PyObject *co
 #ifndef _DECIMAL_CONTEXT_APPLY_METHODDEF
     #define _DECIMAL_CONTEXT_APPLY_METHODDEF
 #endif /* !defined(_DECIMAL_CONTEXT_APPLY_METHODDEF) */
-/*[clinic end generated code: output=75ab23467fa0eee3 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=07ec694c7fd6b048 input=a9049054013a1b77]*/

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]