diff --git a/Doc/builtins/stdtypes.rst b/Doc/builtins/stdtypes.rst index cf05f947c5908df..ef3a6eff3eb0b36 100644 --- a/Doc/builtins/stdtypes.rst +++ b/Doc/builtins/stdtypes.rst @@ -1683,7 +1683,15 @@ application). Lists implement all of the :ref:`common ` and :ref:`mutable ` sequence operations. Lists also provide the - following additional method: + following additional methods: + + .. method:: list.discard(value, /) + + Remove the first item from the list where ``list[i] == value`` + if such an item is present. Return ``True`` if an item was removed, + ``False`` otherwise. + + .. versionadded:: next .. method:: list.sort(*, key=None, reverse=False) diff --git a/Doc/builtins/threadsafety.rst b/Doc/builtins/threadsafety.rst index 32600a35ef6eeaa..e5dddfaa288cb7c 100644 --- a/Doc/builtins/threadsafety.rst +++ b/Doc/builtins/threadsafety.rst @@ -165,9 +165,9 @@ intermediate states since they modify multiple elements in place: lst.pop(idx) # idx not at the end of the list, shifts elements lst *= x # copies elements in place -The :meth:`~list.remove` method may allow concurrent modifications since -element comparison may execute arbitrary Python code (via -:meth:`~object.__eq__`). +The :meth:`~list.remove` and :meth:`~list.discard` methods may allow +concurrent modifications since element comparison may execute arbitrary +Python code (via :meth:`~object.__eq__`). :meth:`~list.extend` is safe to call from multiple threads. However, its guarantees depend on the iterable passed to it. If it is a :class:`list`, a diff --git a/Doc/builtins/time-complexity.rst b/Doc/builtins/time-complexity.rst index 5ce02ac2761b621..444087047a4d3a0 100644 --- a/Doc/builtins/time-complexity.rst +++ b/Doc/builtins/time-complexity.rst @@ -47,6 +47,8 @@ If you need to add or remove at both ends, consider using a - *O*\ (1) * - Delete item (``del l[k]``) [2]_ - *O*\ (*n* - *k*) + * - Discard (``l.discard(x)``, ``l.remove(x)``) + - *O*\ (*n*) * - Iteration - *O*\ (*n*) * - Get slice (``l[i:j]``) diff --git a/Doc/tutorial/datastructures.rst b/Doc/tutorial/datastructures.rst index 77dd8a6e66d5cc4..238ca619e364d27 100644 --- a/Doc/tutorial/datastructures.rst +++ b/Doc/tutorial/datastructures.rst @@ -43,6 +43,13 @@ of the methods of list objects: :exc:`ValueError` if there is no such item. +.. method:: list.discard(value, /) + :noindex: + + Remove the first item from the list whose value is equal to *value*, if there + is such an item. Return ``True`` if an item was removed, ``False`` otherwise. + + .. method:: list.pop(index=-1, /) :noindex: diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e342aa71c7924ea..4742a9efd9af16b 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -107,6 +107,10 @@ Other language changes :func:`asyncio.print_call_graph` can see through them into the awaited code. (Contributed by Kumar Aditya in :gh:`157361`.) +* Added :meth:`list.discard`, which removes the first occurrence of a value + from a list if it is present and returns whether an item was removed. + (Contributed by Stan Ulbrych in :gh:`160724`.) + Default interactive shell ========================= diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index f23129d7e3275e7..00f487916c06ea6 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -1386,6 +1386,9 @@ def pop(self, i=-1): def remove(self, item): self.data.remove(item) + def discard(self, item): + return self.data.discard(item) + def clear(self): self.data.clear() diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index ad9a9ea83035fb4..645627383bfacdc 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -373,6 +373,66 @@ def __eq__(self, other): # verify that original order and values are retained. self.assertIs(x, y) + def test_discard(self): + a = self.type2test([0, 0, 1]) + self.assertIs(a.discard(1), True) + self.assertEqual(a, self.type2test([0, 0])) + self.assertIs(a.discard(0), True) + self.assertEqual(a, self.type2test([0])) + self.assertIs(a.discard(0), True) + self.assertEqual(a, self.type2test([])) + self.assertIs(a.discard(0), False) + self.assertEqual(a, self.type2test([])) + + self.assertRaises(TypeError, a.discard) + + a = self.type2test([1, 2]) + a.discard(NEVER_EQ) + self.assertEqual(a, self.type2test([1, 2])) + a.discard(ALWAYS_EQ) + self.assertEqual(a, self.type2test([2])) + a = self.type2test([ALWAYS_EQ]) + a.discard(1) + self.assertEqual(a, self.type2test([])) + a = self.type2test([ALWAYS_EQ]) + a.discard(NEVER_EQ) + self.assertEqual(a, self.type2test([])) + a = self.type2test([NEVER_EQ]) + a.discard(ALWAYS_EQ) + self.assertEqual(a, self.type2test([NEVER_EQ])) + + class BadExc(Exception): + pass + + class BadCmp: + def __eq__(self, other): + if other == 2: + raise BadExc() + return False + + a = self.type2test([0, 1, 2, 3]) + self.assertRaises(BadExc, a.discard, BadCmp()) + + class BadCmp2: + def __eq__(self, other): + raise BadExc() + + d = self.type2test('abcdefghcij') + d.discard('c') + self.assertEqual(d, self.type2test('abdefghcij')) + d.discard('c') + self.assertEqual(d, self.type2test('abdefghij')) + d.discard('c') + self.assertEqual(d, self.type2test('abdefghij')) + + # Handle comparison errors + d = self.type2test(['a', 'b', BadCmp2(), 'c']) + e = self.type2test(d) + self.assertRaises(BadExc, d.discard, 'c') + for x, y in zip(d, e): + # verify that original order and values are retained. + self.assertIs(x, y) + def test_index(self): super().test_index() a = self.type2test([-2, -1, 0, 0, 1, 2]) diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index 828440a993a975d..16f27bf7ca8ac7c 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -209,6 +209,7 @@ def merge(self, other): 'clear', 'copy', 'count', + 'discard', 'extend', 'index', 'insert', diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 260f9f4f607f152..503082984263892 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -356,6 +356,9 @@ def __eq__(self, other): with self.assertRaises(ValueError): lst.remove(lst) + lst = L([X()]) + lst.discard(lst) + # bpo-39453: list.__contains__ was not holding strong references # to list elements while calling PyObject_RichCompareBool(). lst = [X(), X()] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-17-16-29.gh-issue-160724.23hdnD.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-17-16-29.gh-issue-160724.23hdnD.rst new file mode 100644 index 000000000000000..ac33690e98662f4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-17-16-29.gh-issue-160724.23hdnD.rst @@ -0,0 +1,2 @@ +Add :meth:`list.discard`, which removes the first occurrence of a value from +a list if it is present and returns whether an item was removed. diff --git a/Objects/clinic/listobject.c.h b/Objects/clinic/listobject.c.h index 359b9c202694472..e5a491c1d89066f 100644 --- a/Objects/clinic/listobject.c.h +++ b/Objects/clinic/listobject.c.h @@ -455,6 +455,32 @@ list_remove(PyObject *self, PyObject *value) return return_value; } +PyDoc_STRVAR(list_discard__doc__, +"discard($self, value, /)\n" +"--\n" +"\n" +"Remove first occurrence of value if it is present.\n" +"\n" +"Return True if an item was removed, False otherwise."); + +#define LIST_DISCARD_METHODDEF \ + {"discard", (PyCFunction)list_discard, METH_O, list_discard__doc__}, + +static PyObject * +list_discard_impl(PyListObject *self, PyObject *value); + +static PyObject * +list_discard(PyObject *self, PyObject *value) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = list_discard_impl((PyListObject *)self, value); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(list___init____doc__, "list(iterable=(), /)\n" "--\n" @@ -528,4 +554,4 @@ list___reversed__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return list___reversed___impl((PyListObject *)self); } -/*[clinic end generated code: output=9eab9551fc4cea1c input=a9049054013a1b77]*/ +/*[clinic end generated code: output=fe8e392cf1c521aa input=a9049054013a1b77]*/ diff --git a/Objects/listobject.c b/Objects/listobject.c index a01f40d57181da5..32aa9ae8a505659 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3462,6 +3462,29 @@ list_count_impl(PyListObject *self, PyObject *value) return PyLong_FromSsize_t(count); } +static int +list_remove_lock_held(PyListObject *self, PyObject *value) +{ + Py_ssize_t i; + + for (i = 0; i < Py_SIZE(self); i++) { + PyObject *obj = self->ob_item[i]; + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + if (cmp > 0) { + if (list_ass_slice_lock_held(self, i, i+1, NULL) < 0) { + return -1; + } + return 1; + } + else if (cmp < 0) { + return -1; + } + } + return 0; +} + /*[clinic input] @critical_section list.remove @@ -3478,23 +3501,38 @@ static PyObject * list_remove_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/ { - Py_ssize_t i; + int res = list_remove_lock_held(self, value); + if (res < 0) { + return NULL; + } + if (res == 0) { + PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); + return NULL; + } + Py_RETURN_NONE; +} - for (i = 0; i < Py_SIZE(self); i++) { - PyObject *obj = self->ob_item[i]; - Py_INCREF(obj); - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); - Py_DECREF(obj); - if (cmp > 0) { - if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) - Py_RETURN_NONE; - return NULL; - } - else if (cmp < 0) - return NULL; +/*[clinic input] +@critical_section +list.discard + + value: object + / + +Remove first occurrence of value if it is present. + +Return True if an item was removed, False otherwise. +[clinic start generated code]*/ + +static PyObject * +list_discard_impl(PyListObject *self, PyObject *value) +/*[clinic end generated code: output=4d8c1aee9a101f8c input=6330bc648285d994]*/ +{ + int res = list_remove_lock_held(self, value); + if (res < 0) { + return NULL; } - PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); - return NULL; + return PyBool_FromLong(res); } static int @@ -3685,6 +3723,7 @@ static PyMethodDef list_methods[] = { LIST_EXTEND_METHODDEF LIST_POP_METHODDEF LIST_REMOVE_METHODDEF + LIST_DISCARD_METHODDEF LIST_INDEX_METHODDEF LIST_COUNT_METHODDEF LIST_REVERSE_METHODDEF