|
| 1 | +PEP: 851 |
| 2 | +Title: Add list.discard() |
| 3 | +Author: Stan Ulbrych <stan@python.org> |
| 4 | +Discussions-To: Pending |
| 5 | +Status: Draft |
| 6 | +Type: Standards Track |
| 7 | +Created: 27-Oct-2026 |
| 8 | +Python-Version: 3.16 |
| 9 | +Post-History: `12-Oct-2025 <https://discuss.python.org/t/add-list-discard-and-deque-discard/104330>`__ |
| 10 | + |
| 11 | + |
| 12 | +Abstract |
| 13 | +======== |
| 14 | + |
| 15 | +Add ``list.discard(x)``, which removes the first item equal to *x* if one |
| 16 | +is present and does nothing otherwise, returning whether an item was |
| 17 | +removed. It is the non-raising counterpart to :meth:`list.remove`, as |
| 18 | +:meth:`set.discard` is to :meth:`set.remove`. |
| 19 | + |
| 20 | + |
| 21 | +Motivation |
| 22 | +========== |
| 23 | + |
| 24 | +Removing a value that may not be present is `usually written as |
| 25 | +<https://stackoverflow.com/questions/4915920/how-to-delete-an-item-in-a-list-if-it-exists>`__:: |
| 26 | + |
| 27 | + if x in lst: |
| 28 | + lst.remove(x) |
| 29 | + |
| 30 | +or:: |
| 31 | + |
| 32 | + try: |
| 33 | + lst.remove(x) |
| 34 | + except ValueError: |
| 35 | + pass |
| 36 | + |
| 37 | +The first form is clear but scans the list twice on a hit, and if the list |
| 38 | +is shared between threads another thread can remove the item between the |
| 39 | +two calls, turning a no-op into a :exc:`ValueError`. The second avoids both |
| 40 | +problems but obscures the intent. It is four lines for one operation, pays |
| 41 | +for raising and catching an exception on every miss, and |
| 42 | +``except ValueError`` is broad enough to hide unrelated errors from a |
| 43 | +user-defined :meth:`~object.__eq__`. |
| 44 | + |
| 45 | +Both are common. A search of the 5,000 most downloaded PyPI projects [#search]_ |
| 46 | +finds 1,062 matches for the first form across 411 projects and 331 for |
| 47 | +the second across 164 projects. Some of these are on other container |
| 48 | +types; in the standard library, excluding tests, two of the 29 matches are |
| 49 | +on deques and the remaining seven and 20 are on lists, for example in |
| 50 | +`logging |
| 51 | +<https://github.com/python/cpython/blob/3acc941f2b7288b212a4163ab21aa79faff9cd13/Lib/logging/__init__.py#L840-L841>`__, |
| 52 | +`warnings <https://github.com/python/cpython/blob/3acc941f2b7288b212a4163ab21aa79faff9cd13/Lib/_py_warnings.py#L326-L329>`__, |
| 53 | +`tkinter <https://github.com/python/cpython/blob/3acc941f2b7288b212a4163ab21aa79faff9cd13/Lib/tkinter/__init__.py#L490-L493>`__, |
| 54 | +`runpy <https://github.com/python/cpython/blob/3acc941f2b7288b212a4163ab21aa79faff9cd13/Lib/runpy.py#L312-L315>`__ and |
| 55 | +`asyncio <https://github.com/python/cpython/blob/3acc941f2b7288b212a4163ab21aa79faff9cd13/Lib/asyncio/subprocess.py#L100-L101>`__. |
| 56 | +Each becomes a single call that states the intent directly:: |
| 57 | + |
| 58 | + lst.discard(x) |
| 59 | + |
| 60 | +The cost of providing this is small, as ``discard()`` reuses the existing |
| 61 | +search and removal implementation of :meth:`remove() <list.remove>`. |
| 62 | + |
| 63 | + |
| 64 | +Specification |
| 65 | +============= |
| 66 | + |
| 67 | +``list`` gains a positional-only method ``discard(value, /)``. It removes |
| 68 | +the first item equal to *value* and returns ``True``, or returns ``False`` |
| 69 | +if there is no such item. Only the absence of a matching item is reported |
| 70 | +by the return value; any other error, including a :exc:`ValueError` raised |
| 71 | +during equality comparison, propagates as an exception. The implementation |
| 72 | +shares the search loop of :meth:`remove() <list.remove>`, so which item is |
| 73 | +removed, how values are compared and the thread-safety properties are |
| 74 | +identical to ``remove()``. :class:`collections.UserList` gains the same |
| 75 | +method for consistency. |
| 76 | + |
| 77 | + |
| 78 | +Rationale |
| 79 | +========= |
| 80 | + |
| 81 | +The name follows the ``remove()`` / ``discard()`` distinction already |
| 82 | +made by ``set``, where ``discard()`` is the variant that tolerates a |
| 83 | +missing item. It has been raised that after ``s.discard(x)`` the test |
| 84 | +``x not in s`` holds, while after ``lst.discard(x)`` it need not if *x* |
| 85 | +appears more than once. This is a real difference. ``discard()`` |
| 86 | +deliberately shares the search and removal semantics of :meth:`list.remove` |
| 87 | +rather than those of :meth:`set.discard`, and the documentation for |
| 88 | +``list.discard()`` in the reference implementation states this explicitly. |
| 89 | +The name remains the least surprising choice as users who know |
| 90 | +sets can apply that knowledge directly. |
| 91 | + |
| 92 | +Unlike :meth:`set.discard`, which returns ``None``, a bool is returned. This |
| 93 | +costs nothing, since the implementation already knows the outcome, and lets |
| 94 | +``if lst.discard(x):`` replace the double scan for callers who care. |
| 95 | + |
| 96 | + |
| 97 | +Backwards Compatibility |
| 98 | +======================= |
| 99 | + |
| 100 | +:class:`list` subclasses defining their own ``discard`` keep it. Code using |
| 101 | +``hasattr(obj, "discard")`` to tell sets from lists would be affected. |
| 102 | +None was found in the standard library or the top 5,000 PyPI packages. |
| 103 | + |
| 104 | + |
| 105 | +Security Implications |
| 106 | +===================== |
| 107 | + |
| 108 | +None. |
| 109 | + |
| 110 | + |
| 111 | +How to Teach This |
| 112 | +================= |
| 113 | + |
| 114 | +Introduce it in the `tutorial |
| 115 | +<https://docs.python.org/3/tutorial/datastructures.html#more-on-lists>`__ |
| 116 | +directly after :meth:`list.remove`, with ``remove()`` for when the item |
| 117 | +must be present and ``discard()`` for when it may not be. The same pairing |
| 118 | +already exists for :class:`set`. |
| 119 | + |
| 120 | + |
| 121 | +Reference Implementation |
| 122 | +======================== |
| 123 | + |
| 124 | +A reference implementation can be found in `python/cpython#160726 |
| 125 | +<https://github.com/python/cpython/pull/160726>`__. |
| 126 | + |
| 127 | + |
| 128 | +Rejected Ideas |
| 129 | +============== |
| 130 | + |
| 131 | +A parameter on ``remove()`` |
| 132 | +--------------------------- |
| 133 | + |
| 134 | +A separate method is preferred over a parameter such as |
| 135 | +``remove(x, missing_ok=True)``. The standard library has precedent for both |
| 136 | +shapes, :meth:`Path.unlink(missing_ok=True) <pathlib.Path.unlink>` and |
| 137 | +:func:`os.makedirs(exist_ok=True) <os.makedirs>` on one side and |
| 138 | +:meth:`set.remove` / :meth:`set.discard` on the other, but the closest |
| 139 | +precedent is ``set``, and following it means the two container types share |
| 140 | +one vocabulary. A method also reads more clearly at the call site, since |
| 141 | +``lst.discard(x)`` says what happens while a flag describes what does not. |
| 142 | + |
| 143 | +Adding ``discard()`` to ``collections.abc.MutableSequence`` |
| 144 | +----------------------------------------------------------- |
| 145 | + |
| 146 | +:class:`collections.abc.MutableSequence` is left unchanged, as is the case |
| 147 | +for :meth:`list.copy`. Adding ``discard()`` to the ABC would change the |
| 148 | +interface of every third-party subclass, while virtual subclasses |
| 149 | +registered with ``register()`` would not gain it at all. |
| 150 | + |
| 151 | +``deque.discard()`` |
| 152 | +------------------- |
| 153 | + |
| 154 | +The `original discussion |
| 155 | +<https://discuss.python.org/t/add-list-discard-and-deque-discard/104330>`__ |
| 156 | +also proposed :class:`collections.deque` gaining the same method. It is |
| 157 | +left out of this PEP to keep the proposal to the builtin :class:`list`. |
| 158 | +It can follow as a routine change if this PEP is accepted. |
| 159 | + |
| 160 | + |
| 161 | +Footnotes |
| 162 | +========= |
| 163 | + |
| 164 | +.. [#search] Using `Victor Stinner's scripts |
| 165 | + <https://hugovk.dev/blog/2022/how-to-search-5000-python-projects/>`__, |
| 166 | + modified to match across lines, with the regular expressions:: |
| 167 | +
|
| 168 | + if ([\w.\[\]]+) in ([\w.\[\]]+):\n\s+\2\.remove\(\1\) |
| 169 | +
|
| 170 | + try:\n\s+([\w.\[\]]+)\.remove\(([\w.\[\]]+)\)\n\s*except ValueError: |
| 171 | +
|
| 172 | + The first pattern's backreferences require the same names on both |
| 173 | + lines. The counts are approximate, as the second pattern also matches |
| 174 | + handlers that do something other than ignore the absence, while neither |
| 175 | + matches variants such as compound conditions, |
| 176 | + ``except (ValueError, ...)`` or a comment between the lines. |
| 177 | +
|
| 178 | +
|
| 179 | +Acknowledgements |
| 180 | +================ |
| 181 | + |
| 182 | +Thanks to Guido van Rossum for suggesting that ``discard()`` return whether |
| 183 | +an item was removed. |
| 184 | + |
| 185 | + |
| 186 | +Copyright |
| 187 | +========= |
| 188 | + |
| 189 | +This document is placed in the public domain or under the |
| 190 | +CC0-1.0-Universal license, whichever is more permissive. |
0 commit comments