From 805a3325efa16940c4c46cc17e5505e542860de5 Mon Sep 17 00:00:00 2001 From: Marius-Juston Date: Sun, 1 Feb 2026 15:09:18 -0600 Subject: [PATCH 1/4] implementation of the minmax function --- Lib/test/test_builtin.py | 66 +++++++++++++++ Python/bltinmodule.c | 171 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 7b69374b1868d1..4a6b6e8eeba505 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1630,6 +1630,72 @@ def __getitem__(self, index): self.assertEqual(min(data, key=f), sorted(data, key=f)[0]) + + def test_minmax(self): + self.assertEqual(minmax('123123'), ('1', '3')) + self.assertEqual(minmax(1, 2, 3), (1, 3)) + self.assertEqual(minmax((1, 2, 3, 1, 2, 3)), (1, 3)) + self.assertEqual(minmax([1, 2, 3, 1, 2, 3]), (1, 3)) + + self.assertEqual(minmax(1, 2, 3.0), (1, 3.0)) + self.assertEqual(minmax(1, 2.0, 3), (1, 3)) + self.assertEqual(minmax(1.0, 2, 3), (1.0, 3)) + + with self.assertRaisesRegex( + TypeError, + 'minmax expected at least 1 argument, got 0' + ): + minmax() + + self.assertRaises(TypeError, minmax, 42) + with self.assertRaisesRegex( + ValueError, + r'minmax\(\) iterable argument is empty' + ): + minmax(()) + class BadSeq: + def __getitem__(self, index): + raise ValueError + self.assertRaises(ValueError, minmax, BadSeq()) + + for stmt in ( + "minmax(key=int)", # no args + "minmax(default=None)", + "minmax(1, 2, default=None)", # require container for default + "minmax(default=None, key=int)", + "minmax(1, key=int)", # single arg not iterable + "minmax(1, 2, keystone=int)", # wrong keyword + "minmax(1, 2, key=int, abc=int)", # two many keywords + "minmax(1, 2, key=1)", # keyfunc is not callable + ): + try: + exec(stmt, globals()) + except TypeError: + pass + else: + self.fail(stmt) + + self.assertEqual(minmax((1,), key=neg), (1, 1)) # one elem iterable + self.assertEqual(minmax((1,2), key=neg),(2, 1)) # two elem iterable + self.assertEqual(minmax(1, 2, key=neg), (2, 1)) # two elems + + self.assertEqual(minmax((), default=None), (None, None)) # zero elem iterable + self.assertEqual(minmax((1,), default=None), (1, 1)) # one elem iterable + self.assertEqual(minmax((1,2), default=None), (1, 2)) # two elem iterable + + self.assertEqual(minmax((), default=1, key=neg), (1, 1)) + self.assertEqual(minmax((1, 2), default=1, key=neg), (2, 1)) + + self.assertEqual(minmax((1, 2), key=None), (1, 2)) + + data = [random.randrange(200) for i in range(100)] + keys = dict((elem, random.randrange(50)) for elem in data) + f = keys.__getitem__ + + sorted_vals = sorted(data, key=f) + self.assertEqual(minmax(data, key=f), + (sorted_vals[0], sorted_vals[-1])) + def test_next(self): it = iter(range(2)) self.assertEqual(next(it), 0) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index c2d780ac9b9270..76a3545f02932e 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2114,6 +2114,176 @@ the provided iterable is empty.\n\ With two or more positional arguments, return the largest argument."); +static PyObject * +builtin_minmax(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *it = NULL, *item, *val, *maxitem, *maxval, *minitem, *minval, *minmax_obj, *keyfunc=NULL; + PyObject *defaultval = NULL; + static const char * const keywords[] = {"key", "default", NULL}; + static _PyArg_Parser _parser = {"|$OO:minmax", keywords, 0}; + + if (nargs == 0) { + PyErr_Format(PyExc_TypeError, "minmax expected at least 1 argument, got 0"); + return NULL; + } + + if (kwnames != NULL && !_PyArg_ParseStackAndKeywords(args + nargs, 0, kwnames, &_parser, + &keyfunc, &defaultval)) { + return NULL; + } + + const int positional = nargs > 1; // False iff nargs == 1 + if (positional && defaultval != NULL) { + PyErr_Format(PyExc_TypeError, + "Cannot specify a default for minmax() with multiple " + "positional arguments"); + return NULL; + } + + if (!positional) { + it = PyObject_GetIter(args[0]); + if (it == NULL) { + return NULL; + } + } + + if (keyfunc == Py_None) { + keyfunc = NULL; + } + + maxitem = NULL; /* the max result */ + maxval = NULL; /* the value associated with the max result */ + + minitem = NULL; /* the min result */ + minval = NULL; /* the value associated with the min result */ + + while (1) { + if (it == NULL) { + if (nargs-- <= 0) { + break; + } + item = *args++; + Py_INCREF(item); + } + else { + item = PyIter_Next(it); + if (item == NULL) { + if (PyErr_Occurred()) { + goto Fail_it; + } + break; + } + } + + /* get the value from the key function */ + if (keyfunc != NULL) { + val = PyObject_CallOneArg(keyfunc, item); + if (val == NULL) + goto Fail_it_item; + } + /* no key function; the value is the item */ + else { + val = Py_NewRef(item); + } + + /* minimum/maximum value and item are unset; set them */ + if (maxval == NULL || minval == NULL) { + maxitem = item; + maxval = val; + + minitem = Py_NewRef(item); + minval = Py_NewRef(val); + } + /* minimum/maximum value and item are set; update them as necessary */ + else { + /* check for new minimum value */ + const int cmp_lt = PyObject_RichCompareBool(val, minval, Py_LT); + + if (cmp_lt < 0) { + goto Fail_it_item_and_val; + } + + if (cmp_lt > 0) { + Py_DECREF(minitem); + Py_DECREF(minval); + + minval = val; + minitem = item; + } else { + /* Since we did not get a new minimum it could be a new maximum instead */ + const int cmp_gt = PyObject_RichCompareBool(val, maxval, Py_GT); + + if (cmp_gt < 0) { + goto Fail_it_item_and_val; + } + + if(cmp_gt > 0) { + Py_DECREF(maxitem); + Py_DECREF(maxval); + + maxval = val; + maxitem = item; + } + else { + Py_DECREF(item); + Py_DECREF(val); + } + } + } + } + if (maxval == NULL || minval == NULL) { + assert(maxitem == NULL); + assert(minitem == NULL); + if (defaultval != NULL) { + maxitem = Py_NewRef(defaultval); + minitem = Py_NewRef(defaultval); + } else { + PyErr_Format(PyExc_ValueError, + "minmax() iterable argument is empty"); + + goto Fail_it; + } + }else { + Py_DECREF(maxval); + Py_DECREF(minval); + } + + Py_XDECREF(it); + + if ((minmax_obj = PyTuple_New(2)) == NULL) { + goto Fail_it; + } + + PyTuple_SET_ITEM(minmax_obj, 0, minitem); + PyTuple_SET_ITEM(minmax_obj, 1, maxitem); + + return minmax_obj; + +Fail_it_item_and_val: + Py_DECREF(val); +Fail_it_item: + Py_DECREF(item); +Fail_it: + Py_XDECREF(maxval); + Py_XDECREF(maxitem); + + Py_XDECREF(minval); + Py_XDECREF(minitem); + + Py_XDECREF(it); + return NULL; +} + +PyDoc_STRVAR(minmax_doc, +"minmax(iterable, *[, default=obj, key=func]) -> (min_value, max_value)\n\ +minmax(arg1, arg2, *args, *[, key=func]) -> (min_value, max_value)\n\ +\n\ +With a single iterable argument, return both its smallest and biggest item. The\n\ +default keyword-only argument specifies an object to return if\n\ +the provided iterable is empty.\n\ +With two or more positional arguments, return the smallest and largest argument."); + + /*[clinic input] oct as builtin_oct @@ -3392,6 +3562,7 @@ static PyMethodDef builtin_methods[] = { BUILTIN_LOCALS_METHODDEF {"max", _PyCFunction_CAST(builtin_max), METH_FASTCALL | METH_KEYWORDS, max_doc}, {"min", _PyCFunction_CAST(builtin_min), METH_FASTCALL | METH_KEYWORDS, min_doc}, + {"minmax", _PyCFunction_CAST(builtin_minmax), METH_FASTCALL | METH_KEYWORDS, minmax_doc}, {"next", _PyCFunction_CAST(builtin_next), METH_FASTCALL, next_doc}, BUILTIN_ANEXT_METHODDEF BUILTIN_OCT_METHODDEF From 76690aed2e873d925935934bcde582c6f500473d Mon Sep 17 00:00:00 2001 From: Marius-Juston Date: Sun, 1 Feb 2026 15:31:44 -0600 Subject: [PATCH 2/4] added documentation and blurb --- Doc/howto/sorting.rst | 5 +++++ Doc/library/functions.rst | 22 +++++++++++++++++++ ...-02-01-15-31-11.gh-issue-144382.EyosHQ.rst | 4 ++++ 3 files changed, 31 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst diff --git a/Doc/howto/sorting.rst b/Doc/howto/sorting.rst index 70c34cde8a0659..bb82b79f6c74ed 100644 --- a/Doc/howto/sorting.rst +++ b/Doc/howto/sorting.rst @@ -400,6 +400,11 @@ library provides several tools that do less work than a full sort: respectively. These functions make a single pass over the input data and require almost no auxiliary memory. +* :func:`minmax` returns both the smallest and largest values, +respectively. This functions make a single pass over the input data and +require almost no auxiliary memory. This function is useful if both `min` and +`max` need to be called and thus is more efficient for larger datasets. + * :func:`heapq.nsmallest` and :func:`heapq.nlargest` return the *n* smallest and largest values, respectively. These functions make a single pass over the data keeping only *n* elements in memory diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index cd819b8d06480a..139754f209c6f1 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1295,6 +1295,28 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.8 The *key* can be ``None``. +.. function:: minmax(iterable, /, *, key=None) + minmax(iterable, /, *, default, key=None) + minmax(arg1, arg2, /, *args, key=None) + + Return the smallest and largest items respectively in an iterable or the smallest and largest + of two or more arguments. + + If one positional argument is provided, it should be an :term:`iterable`. + The smallest and largest items in the iterable are returned. If two or more positional + arguments are provided, the smallest and largest of the positional arguments are + returned. + + There are two optional keyword-only arguments. The *key* argument specifies + a one-argument ordering function like that used for :meth:`list.sort`. The + *default* argument specifies an object to return if the provided iterable is + empty. If the iterable is empty and *default* is not provided, a + :exc:`ValueError` is raised. + + If multiple items are minimal / maximal, the function returns the first one + encountered. This is consistent with other sort-stability preserving tools + such as ``(sorted(iterable, key=keyfunc)[0], sorted(iterable, key=keyfunc)[-1])`` + and ``(heapq.nsmallest(1,iterable, key=keyfunc), heapq.nlargest(1,iterable, key=keyfunc))``. .. function:: next(iterator, /) next(iterator, default, /) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst new file mode 100644 index 00000000000000..d45799551aa3f9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst @@ -0,0 +1,4 @@ +This creates a new builtin function called `minmax`, which does work similar +to the functions `min` and `max`; however, is more efficient than running +`min` and then `max` and computes the smallest and largest elements of the +iterable in a single pass; rather than 2 passes. From d85c9f590964c73eeb79d5b1e62d0c9b279e6ad5 Mon Sep 17 00:00:00 2001 From: Marius-Juston Date: Sun, 1 Feb 2026 15:35:23 -0600 Subject: [PATCH 3/4] used double backticks for inline literals --- .../2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst index d45799551aa3f9..1999c0f06b01e9 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-01-15-31-11.gh-issue-144382.EyosHQ.rst @@ -1,4 +1,4 @@ -This creates a new builtin function called `minmax`, which does work similar -to the functions `min` and `max`; however, is more efficient than running -`min` and then `max` and computes the smallest and largest elements of the +This creates a new builtin function called ``minmax``, which does work similar +to the functions ``min`` and ``max``; however, is more efficient than running +``min`` and then ``max`` and computes the smallest and largest elements of the iterable in a single pass; rather than 2 passes. From 52920398701e0653a0f1aef2b1ea936f4574e583 Mon Sep 17 00:00:00 2001 From: Marius-Juston Date: Sun, 1 Feb 2026 15:38:39 -0600 Subject: [PATCH 4/4] updated indent and function highlighting --- Doc/howto/sorting.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/howto/sorting.rst b/Doc/howto/sorting.rst index bb82b79f6c74ed..3ec37bcfcaab4f 100644 --- a/Doc/howto/sorting.rst +++ b/Doc/howto/sorting.rst @@ -401,9 +401,9 @@ library provides several tools that do less work than a full sort: require almost no auxiliary memory. * :func:`minmax` returns both the smallest and largest values, -respectively. This functions make a single pass over the input data and -require almost no auxiliary memory. This function is useful if both `min` and -`max` need to be called and thus is more efficient for larger datasets. + respectively. This functions make a single pass over the input data and + require almost no auxiliary memory. This function is useful if both :meth:`min` and + :meth:`max` need to be called and thus is more efficient for larger datasets. * :func:`heapq.nsmallest` and :func:`heapq.nlargest` return the *n* smallest and largest values, respectively. These functions