From 80088bf411a9d86b09fec88b49bc01a323e03b70 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 8 Jul 2026 21:42:02 +0200 Subject: [PATCH 01/17] Add a 'Time complexity of operations on built-in types' section --- Doc/faq/design.rst | 8 + Doc/faq/programming.rst | 2 +- Doc/glossary.rst | 2 +- Doc/library/collections.rst | 3 +- Doc/library/index.rst | 1 + Doc/library/stdtypes.rst | 6 + Doc/library/time-complexity.rst | 304 ++++++++++++++++++++++++++++++++ Doc/tutorial/datastructures.rst | 3 +- 8 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 Doc/library/time-complexity.rst diff --git a/Doc/faq/design.rst b/Doc/faq/design.rst index c914089e9806ec6..02a3a3a22070dae 100644 --- a/Doc/faq/design.rst +++ b/Doc/faq/design.rst @@ -430,6 +430,8 @@ tuples, but not lists, can be used as keys. Note, however, that a tuple is only hashable if all of its elements are hashable. +.. _how-are-lists-implemented: + How are lists implemented in CPython? ------------------------------------- @@ -445,6 +447,10 @@ cleverness is applied to improve the performance of appending items repeatedly; when the array must be grown, some extra space is allocated so the next few times don't require an actual resize. +See :ref:`time-complexity` for the costs of the various list operations. + + +.. _how-are-dictionaries-implemented: How are dictionaries implemented in CPython? -------------------------------------------- @@ -462,6 +468,8 @@ internal array where the value will be stored. Assuming that you're storing keys that all have different hash values, this means that dictionaries take constant time -- *O*\ (1), in Big-O notation -- to retrieve a key. +See :ref:`time-complexity` for the costs of the various dictionary operations. + Why must dictionary keys be immutable? -------------------------------------- diff --git a/Doc/faq/programming.rst b/Doc/faq/programming.rst index c2f8f72ee1f2c4b..4e1157e6ebe7296 100644 --- a/Doc/faq/programming.rst +++ b/Doc/faq/programming.rst @@ -1136,7 +1136,7 @@ What is the most efficient way to concatenate many strings together? :class:`str` and :class:`bytes` objects are immutable, therefore concatenating many strings together is inefficient as each concatenation creates a new object. In the general case, the total runtime cost is quadratic in the -total string length. +total string length. See :ref:`time-complexity` for more information. To accumulate many :class:`str` objects, the recommended idiom is to place them into a list and call :meth:`str.join` at the end:: diff --git a/Doc/glossary.rst b/Doc/glossary.rst index bb00a4f02f0efd5..02f89b419d9fc2d 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -942,7 +942,7 @@ Glossary list A built-in Python :term:`sequence`. Despite its name it is more akin to an array in other languages than to a linked list since access to - elements is *O*\ (1). + elements is *O*\ (1). See :ref:`time-complexity`. list comprehension A compact way to process all or part of the elements in a sequence and diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index d09a6c92bbd37dc..65128149de33ec0 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -473,7 +473,8 @@ or subtracting from an empty counter. Though :class:`list` objects support similar operations, they are optimized for fast fixed-length operations and incur *O*\ (*n*) memory movement costs for ``pop(0)`` and ``insert(0, v)`` operations which change both the size and - position of the underlying data representation. + position of the underlying data representation. See :ref:`time-complexity` + for more information. If *maxlen* is not specified or is ``None``, deques may grow to an diff --git a/Doc/library/index.rst b/Doc/library/index.rst index 8fc77be520d4268..f28c03e2fae092f 100644 --- a/Doc/library/index.rst +++ b/Doc/library/index.rst @@ -44,6 +44,7 @@ the `Python Package Index `_. stdtypes.rst exceptions.rst threadsafety.rst + time-complexity.rst text.rst binary.rst diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 886648e820f071d..b2f0d934980353a 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -999,6 +999,9 @@ The ``in`` and ``not in`` operations have the same priorities as the comparison operations. The ``+`` (concatenation) and ``*`` (repetition) operations have the same priority as the corresponding numeric operations. [3]_ +See :ref:`time-complexity` for the costs of the various sequence +operations. + .. index:: triple: operations on; sequence; types pair: built-in function; len @@ -5112,6 +5115,7 @@ computing mathematical operations such as intersection, union, difference, and symmetric difference. (For other containers see the built-in :class:`dict`, :class:`list`, and :class:`tuple` classes, and the :mod:`collections` module.) +See :ref:`time-complexity` for the costs of the various set operations. Like other collections, sets support ``x in set``, ``len(set)``, and ``for x in set``. Being an unordered collection, sets do not record element position or @@ -5336,6 +5340,8 @@ There are currently two standard mapping types, the :dfn:`dictionary` and (For other containers see the built-in :class:`list`, :class:`set`, and :class:`tuple` classes, and the :mod:`collections` module.) +See :ref:`time-complexity` for the costs of the various dictionary +operations. A dictionary's keys are *almost* arbitrary values. Values that are not :term:`hashable`, that is, values containing lists, dictionaries or other diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst new file mode 100644 index 000000000000000..ad9101f2c1524ac --- /dev/null +++ b/Doc/library/time-complexity.rst @@ -0,0 +1,304 @@ +.. _time-complexity: + +=============================================== +Time complexity of operations on built-in types +=============================================== + +This page documents the time-complexity of various operations on built-in types +in CPython. Other Python implementations may have different performance +characteristics. Additionally, the listed costs assume exact built-in types as +instances of subclasses often miss CPython's internal fast paths. + +We use |big O notation|_ to describe how the running time of an operation grows +with the size of its input. Generally, *n* is the number of elements currently +in the container, and *k* is either the value of a parameter or the number of +elements in the parameter. See Ned Batchelder's `Big-O: How Code Slows as Data +Grows `__ talk and blog post for more information. + +.. |big O notation| replace:: Big *O* notation +.. _big O notation: https://en.wikipedia.org/wiki/Big_O_notation + + +:class:`!list` +============== + +Lists are mutable sequences. Internally, a :class:`list` is represented as an +array; for more detail see :ref:`how-are-lists-implemented`. The largest costs +come from growing beyond the current allocation size (because everything must move), +or from inserting or deleting somewhere near the beginning (because everything +after that must move). If you need to add or remove at both ends, +consider using a :class:`collections.deque` instead. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - Copy + - *O*\ (*n*) + * - Append [1]_ + - *O*\ (1) + * - Pop [1]_ [2]_ + - *O*\ (*n* - *k*) + * - Insert [1]_ [2]_ + - *O*\ (*n* - *k*) + * - Get item + - *O*\ (1) + * - Set item + - *O*\ (1) + * - Delete item [2]_ + - *O*\ (*n* - *k*) + * - Iteration + - *O*\ (*n*) + * - Get slice + - *O*\ (*k*) + * - Set slice + - *O*\ (*k* + *n*) + * - Delete slice + - *O*\ (*n*) + * - Extend [1]_ + - *O*\ (*k*) + * - Sort [3]_ + - *O*\ (*n* log *n*) + * - Multiply + - *O*\ (*nk*) + * - ``x in s`` + - *O*\ (*n*) + * - ``min(s)``, ``max(s)`` + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!tuple` +=============== + +A :class:`tuple` is an :term:`immutable` sequence. Because a tuple can never +change, there are no insertion or deletion costs, and making a copy is constant +time as the same object is returned. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - Copy + - *O*\ (1) + * - Get item + - *O*\ (1) + * - Get slice + - *O*\ (*k*) + * - Concatenate (``s + t``) + - *O*\ (*n* + *k*) + * - Multiply + - *O*\ (*nk*) + * - Iteration + - *O*\ (*n*) + * - ``x in s`` + - *O*\ (*n*) + * - ``min(s)``, ``max(s)`` + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!dict`, :class:`!frozendict` +==================================== + +The times listed for dict objects are average-case times, as they assume the +hash function for the objects is sufficiently robust to make collisions +uncommon; they also assume the keys used in parameters are selected uniformly +at random from the set of all keys. In the worst case, when every key hashes +to the same value, each of the *O*\ (1) operations below instead takes +*O*\ (*n*) time. For more detail on the implementation, see +:ref:`how-are-dictionaries-implemented`. + +A :class:`frozendict` is immutable, but the non-mutating operations below +apply to it at the same costs. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - ``k in d`` + - *O*\ (1) + * - Copy [5]_ [6]_ + - *O*\ (*n*) + * - Get item + - *O*\ (1) + * - Set item [1]_ + - *O*\ (1) + * - Delete item + - *O*\ (1) + * - Iteration [6]_ + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!set`, :class:`!frozenset` +================================== + +See :class:`dict` -- the :class:`set` and :class:`frozenset` implementation is +intentionally very similar, and the same hash collision caveat applies: +in the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, +and operations that look up every element of an operand degrade accordingly. + +A frozenset is immutable, but the non-mutating operations below apply to it at +the same costs. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - ``x in s`` + - *O*\ (1) + * - Add (``s.add(x)``) [1]_ + - *O*\ (1) + * - Discard (``s.discard(x)``) + - *O*\ (1) + * - Union (``s | t``) [6]_ + - *O*\ (len(*s*) + len(*t*)) + * - Intersection (``s & t``, ``s.intersection(t)``) [6]_ [7]_ + - *O*\ (min(len(*s*), len(*t*))) + * - Difference (``s - t``, ``s.difference(t)``) [6]_ [8]_ + - *O*\ (len(*s*)) + * - Difference update (``s.difference_update(t)``) [1]_ [6]_ [7]_ + - *O*\ (min(len(*s*), len(*t*))) + * - Symmetric difference (``s ^ t``) [6]_ + - *O*\ (len(*s*) + len(*t*)) + * - Symmetric difference update (``s.symmetric_difference_update(t)``) [1]_ [6]_ + - *O*\ (len(*t*)) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!str`, :class:`!bytes`, :class:`!bytearray` +=================================================== + +:class:`str` and :class:`bytes` objects are immutable sequences of characters and +bytes respectively; as with tuples, copying one returns the original object. +A :class:`bytearray` is mutable, and additionally supports the mutating operations +of :class:`list` (except :meth:`!sort`), at the same costs. However, deleting at +the front only advances the start of the buffer instead of moving the remaining +bytes, and is amortized *O*\ (1). + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - Get item + - *O*\ (1) + * - Get slice + - *O*\ (*k*) + * - Concatenate (``s + t``) + - *O*\ (*n* + *k*) + * - Multiply + - *O*\ (*nk*) + * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [9]_ + - *O*\ (*n*) + * - Encode or decode + - *O*\ (*n*) + * - Iteration + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!memoryview` +==================== + +:class:`memoryview` objects allow Python code to access the internal data +of an object that supports the :ref:`buffer protocol ` without +copying. In particular, slicing a memory view returns a new view onto the same +buffer. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - Create + - *O*\ (1) + * - Get item + - *O*\ (1) + * - Get slice + - *O*\ (1) + * - Convert to bytes (``v.tobytes()``, ``bytes(v)``) + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +:class:`!range` +=============== + +A :class:`range` object computes its items on demand from its *start*, *stop* and +*step* values, so most operations do not depend on the length of the range. + +.. list-table:: + :header-rows: 1 + + * - Operation + - Complexity + * - Get item + - *O*\ (1) + * - Get slice + - *O*\ (1) + * - ``x in s`` [10]_ + - *O*\ (1) + * - Index and count (``s.index(x)``, ``s.count(x)``) [10]_ + - *O*\ (1) + * - Iteration + - *O*\ (*n*) + * - ``min(s)``, ``max(s)`` + - *O*\ (*n*) + * - Get length [4]_ + - *O*\ (1) + + +Notes +===== + +.. [1] Amortized. An individual operation may occasionally be *O*\ (*n*) + when the underlying storage is resized, but this cost is spread over + many operations, depending on the history of the container. + +.. [2] Popping or deleting the element at index *k* of a list of size *n* + shifts all elements after *k* one slot to the left, moving *n* - *k* - 1 + elements; inserting at index *k* shifts the elements from *k* onwards one + slot to the right, moving *n* - *k* elements. The worst case is index 0, + where the whole rest of the list has to be moved; the average case, an + index in the middle of the list, takes *O*\ (*n*/2) = *O*\ (*n*) + operations; and operating at the end of the list moves nothing and is + *O*\ (1). + +.. [3] This is the worst case scenario. Sorting is adaptive and input that is + already sorted or reverse-sorted takes only *O*\ (*n*) comparisons; see + :source:`Objects/listsort.txt` for more information. + +.. [4] The number of elements is stored in the object, so ``len()`` does + not need to count them. + +.. [5] Copying a :class:`frozendict` is *O*\ (1) as it returns the original object. + +.. [6] These operations scan the container's internal hash table, which is + not shrunk when elements are removed. After removing most elements, they + still take time proportional to the container's former size, until a + later insertion triggers a resize. + +.. [7] *O*\ (len(*t*)) if *t* is not a set. + +.. [8] *O*\ (len(*s*) + len(*t*)) if *t* is not a set. + +.. [9] A naive substring search would need *O*\ (*nk*) comparisons in the + worst case, where *k* is the length of the substring searched for, but + CPython uses search algorithms with a linear worst case for forward + searches; see :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` + for details. + +.. [10] Assuming :class:`int` or :class:`bool` arguments. For other types, + the range is searched like any other sequence in *O*\ (*n*) time. diff --git a/Doc/tutorial/datastructures.rst b/Doc/tutorial/datastructures.rst index 276e31a3056f0ee..77dd8a6e66d5cc4 100644 --- a/Doc/tutorial/datastructures.rst +++ b/Doc/tutorial/datastructures.rst @@ -167,7 +167,8 @@ It is also possible to use a list as a queue, where the first element added is the first element retrieved ("first-in, first-out"); however, lists are not efficient for this purpose. While appends and pops from the end of list are fast, doing inserts or pops from the beginning of a list is slow (because all -of the other elements have to be shifted by one). +of the other elements have to be shifted by one). See +:ref:`time-complexity` for more information. To implement a queue, use :class:`collections.deque` which was designed to have fast appends and pops from both ends. For example:: From 84b95f997c54664fd57ba7ddff57734bcff9f7b6 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 8 Jul 2026 22:06:37 +0200 Subject: [PATCH 02/17] Ned's suggestion for blog post rec. --- Doc/library/time-complexity.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index ad9101f2c1524ac..538f10ffdee734a 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -12,8 +12,9 @@ instances of subclasses often miss CPython's internal fast paths. We use |big O notation|_ to describe how the running time of an operation grows with the size of its input. Generally, *n* is the number of elements currently in the container, and *k* is either the value of a parameter or the number of -elements in the parameter. See Ned Batchelder's `Big-O: How Code Slows as Data -Grows `__ talk and blog post for more information. +elements in the parameter. For a pragmatic approach to assessing time complexity, +see Ned Batchelder's `Big-O: How Code Slows as Data Grows +`__ talk and blog post. .. |big O notation| replace:: Big *O* notation .. _big O notation: https://en.wikipedia.org/wiki/Big_O_notation From 19822027cce3e6b660863781265e9b5f4bfc03d0 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 21 Jul 2026 15:36:46 +0200 Subject: [PATCH 03/17] Revert that --- Doc/library/collections.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 65128149de33ec0..d09a6c92bbd37dc 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -473,8 +473,7 @@ or subtracting from an empty counter. Though :class:`list` objects support similar operations, they are optimized for fast fixed-length operations and incur *O*\ (*n*) memory movement costs for ``pop(0)`` and ``insert(0, v)`` operations which change both the size and - position of the underlying data representation. See :ref:`time-complexity` - for more information. + position of the underlying data representation. If *maxlen* is not specified or is ``None``, deques may grow to an From 64fb553c9e51160a826d8cdce678732029ca1918 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 21 Jul 2026 15:40:10 +0200 Subject: [PATCH 04/17] Little fixups --- Doc/library/time-complexity.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 538f10ffdee734a..1fef65d476363e8 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -141,13 +141,13 @@ apply to it at the same costs. :class:`!set`, :class:`!frozenset` ================================== -See :class:`dict` -- the :class:`set` and :class:`frozenset` implementation is -intentionally very similar, and the same hash collision caveat applies: -in the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, +See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are +intentionally very similar, and the same hash collision caveat applies. +In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, and operations that look up every element of an operand degrade accordingly. -A frozenset is immutable, but the non-mutating operations below apply to it at -the same costs. +A :class:`frozenset` is :term:`immutable`, but the non-mutating operations below +apply to it at the same costs. .. list-table:: :header-rows: 1 @@ -180,7 +180,7 @@ the same costs. =================================================== :class:`str` and :class:`bytes` objects are immutable sequences of characters and -bytes respectively; as with tuples, copying one returns the original object. +bytes respectively; As with tuples, copying one returns the original object. A :class:`bytearray` is mutable, and additionally supports the mutating operations of :class:`list` (except :meth:`!sort`), at the same costs. However, deleting at the front only advances the start of the buffer instead of moving the remaining @@ -278,8 +278,8 @@ Notes *O*\ (1). .. [3] This is the worst case scenario. Sorting is adaptive and input that is - already sorted or reverse-sorted takes only *O*\ (*n*) comparisons; see - :source:`Objects/listsort.txt` for more information. + already sorted or reverse-sorted takes only *O*\ (*n*) comparisons. + See :source:`Objects/listsort.txt` for more information. .. [4] The number of elements is stored in the object, so ``len()`` does not need to count them. @@ -298,7 +298,7 @@ Notes .. [9] A naive substring search would need *O*\ (*nk*) comparisons in the worst case, where *k* is the length of the substring searched for, but CPython uses search algorithms with a linear worst case for forward - searches; see :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` + searches. See :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` for details. .. [10] Assuming :class:`int` or :class:`bool` arguments. For other types, From 0392d7e57b74fcb1357c0fcf7315dbc549339fb2 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 21 Jul 2026 22:43:22 +0200 Subject: [PATCH 05/17] =?UTF-8?q?Apply=20review=20suggestions=20from=20Pie?= =?UTF-8?q?ter=20and=20B=C3=A9n=C3=A9dikt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Pieter Eendebak --- Doc/library/time-complexity.rst | 111 ++++++++++++++++---------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 1fef65d476363e8..8a73c52d6e54980 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -6,15 +6,17 @@ Time complexity of operations on built-in types This page documents the time-complexity of various operations on built-in types in CPython. Other Python implementations may have different performance -characteristics. Additionally, the listed costs assume exact built-in types as -instances of subclasses often miss CPython's internal fast paths. +characteristics. Additionally, the listed costs assume exact built-in types, as +instances of subclasses may have different costs. We use |big O notation|_ to describe how the running time of an operation grows -with the size of its input. Generally, *n* is the number of elements currently -in the container, and *k* is either the value of a parameter or the number of -elements in the parameter. For a pragmatic approach to assessing time complexity, -see Ned Batchelder's `Big-O: How Code Slows as Data Grows -`__ talk and blog post. +with the size of its input. Unless stated otherwise, *n* denotes the number of +elements currently in the container, and *k* is either the value of a parameter +or the number of elements in the parameter. + +For a pragmatic approach to assessing time complexity, see Ned Batchelder's +`Big-O: How Code Slows as Data Grows `__ +talk and blog post. .. |big O notation| replace:: Big *O* notation .. _big O notation: https://en.wikipedia.org/wiki/Big_O_notation @@ -23,51 +25,51 @@ see Ned Batchelder's `Big-O: How Code Slows as Data Grows :class:`!list` ============== -Lists are mutable sequences. Internally, a :class:`list` is represented as an -array; for more detail see :ref:`how-are-lists-implemented`. The largest costs -come from growing beyond the current allocation size (because everything must move), -or from inserting or deleting somewhere near the beginning (because everything -after that must move). If you need to add or remove at both ends, -consider using a :class:`collections.deque` instead. +Lists are mutable sequences; for more detail on the implementation see +:ref:`how-are-lists-implemented`. The largest costs come from growing beyond the +current allocation size (because everything must move), or from inserting or +deleting somewhere near the beginning (because everything after that must move). +If you need to add or remove at both ends, consider using a +:class:`collections.deque` instead. .. list-table:: :header-rows: 1 * - Operation - Complexity - * - Copy + * - Copy (``s.copy()``) - *O*\ (*n*) - * - Append [1]_ + * - Append (``s.append(x)``) [1]_ - *O*\ (1) - * - Pop [1]_ [2]_ + * - Pop (``s.pop(k)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Insert [1]_ [2]_ + * - Insert (``s.insert(k, x)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Get item + * - Get item (``s[k]``) - *O*\ (1) - * - Set item + * - Set item (``s[k] = x``) - *O*\ (1) - * - Delete item [2]_ + * - Delete item (``del s[k]``) [2]_ - *O*\ (*n* - *k*) * - Iteration - *O*\ (*n*) - * - Get slice + * - Get slice (``s[i:j]``) - *O*\ (*k*) - * - Set slice + * - Set slice (``s[i:j] = t``) - *O*\ (*k* + *n*) - * - Delete slice + * - Delete slice (``del s[i:j]``) - *O*\ (*n*) - * - Extend [1]_ + * - Extend (``s.extend(t)``) [1]_ - *O*\ (*k*) - * - Sort [3]_ + * - Sort (``s.sort()``) [3]_ - *O*\ (*n* log *n*) - * - Multiply + * - Multiply (``s * k``) - *O*\ (*nk*) * - ``x in s`` - *O*\ (*n*) * - ``min(s)``, ``max(s)`` - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(s)``) [4]_ - *O*\ (1) @@ -83,15 +85,15 @@ time as the same object is returned. * - Operation - Complexity - * - Copy + * - Copy (``tuple(s)``) - *O*\ (1) - * - Get item + * - Get item (``s[k]``) - *O*\ (1) - * - Get slice + * - Get slice (``s[i:j]``) - *O*\ (*k*) * - Concatenate (``s + t``) - *O*\ (*n* + *k*) - * - Multiply + * - Multiply (``s * k``) - *O*\ (*nk*) * - Iteration - *O*\ (*n*) @@ -99,7 +101,7 @@ time as the same object is returned. - *O*\ (*n*) * - ``min(s)``, ``max(s)`` - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(s)``) [4]_ - *O*\ (1) @@ -114,27 +116,27 @@ to the same value, each of the *O*\ (1) operations below instead takes *O*\ (*n*) time. For more detail on the implementation, see :ref:`how-are-dictionaries-implemented`. -A :class:`frozendict` is immutable, but the non-mutating operations below -apply to it at the same costs. +A :class:`frozendict` is immutable, so it does not support setting or deleting +items; the other operations below apply to it at the same costs. .. list-table:: :header-rows: 1 * - Operation - Complexity - * - ``k in d`` + * - ``key in d`` - *O*\ (1) - * - Copy [5]_ [6]_ + * - Copy (``d.copy()``) [5]_ [6]_ - *O*\ (*n*) - * - Get item + * - Get item (``d[key]``) - *O*\ (1) - * - Set item [1]_ + * - Set item (``d[key] = value``) [1]_ - *O*\ (1) - * - Delete item + * - Delete item (``del d[key]``) - *O*\ (1) * - Iteration [6]_ - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(d)``) [4]_ - *O*\ (1) @@ -146,8 +148,9 @@ intentionally very similar, and the same hash collision caveat applies. In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, and operations that look up every element of an operand degrade accordingly. -A :class:`frozenset` is :term:`immutable`, but the non-mutating operations below -apply to it at the same costs. +A :class:`frozenset` is :term:`immutable`, so it does not support adding, +discarding, or the in-place update operations; the others below apply to it at +the same costs. .. list-table:: :header-rows: 1 @@ -172,7 +175,7 @@ apply to it at the same costs. - *O*\ (len(*s*) + len(*t*)) * - Symmetric difference update (``s.symmetric_difference_update(t)``) [1]_ [6]_ - *O*\ (len(*t*)) - * - Get length [4]_ + * - Get length (``len(s)``) [4]_ - *O*\ (1) @@ -191,13 +194,13 @@ bytes, and is amortized *O*\ (1). * - Operation - Complexity - * - Get item + * - Get item (``s[k]``) - *O*\ (1) - * - Get slice + * - Get slice (``s[i:j]``) - *O*\ (*k*) * - Concatenate (``s + t``) - *O*\ (*n* + *k*) - * - Multiply + * - Multiply (``s * k``) - *O*\ (*nk*) * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [9]_ - *O*\ (*n*) @@ -205,7 +208,7 @@ bytes, and is amortized *O*\ (1). - *O*\ (*n*) * - Iteration - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(s)``) [4]_ - *O*\ (1) @@ -222,15 +225,15 @@ buffer. * - Operation - Complexity - * - Create + * - Create (``memoryview(obj)``) - *O*\ (1) - * - Get item + * - Get item (``v[k]``) - *O*\ (1) - * - Get slice + * - Get slice (``v[i:j]``) - *O*\ (1) * - Convert to bytes (``v.tobytes()``, ``bytes(v)``) - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(v)``) [4]_ - *O*\ (1) @@ -245,9 +248,9 @@ A :class:`range` object computes its items on demand from its *start*, *stop* an * - Operation - Complexity - * - Get item + * - Get item (``s[k]``) - *O*\ (1) - * - Get slice + * - Get slice (``s[i:j]``) - *O*\ (1) * - ``x in s`` [10]_ - *O*\ (1) @@ -257,7 +260,7 @@ A :class:`range` object computes its items on demand from its *start*, *stop* an - *O*\ (*n*) * - ``min(s)``, ``max(s)`` - *O*\ (*n*) - * - Get length [4]_ + * - Get length (``len(s)``) [4]_ - *O*\ (1) From 9e1a82a832ac39e853953c5d2f9a67e5635c4915 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 6 Aug 2026 18:49:12 +0200 Subject: [PATCH 06/17] Address Pieter's very helpful review --- Doc/library/stdtypes.rst | 2 ++ Doc/library/time-complexity.rst | 40 +++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index b2f0d934980353a..5df1bae984c572c 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -1124,6 +1124,8 @@ Notes: "end" values (which end depends on the sign of *k*). Note, *k* cannot be zero. If *k* is ``None``, it is treated like ``1``. +.. _typesseq-repeated-concatenation: + (6) Concatenating immutable sequences always results in a new object. This means that building up a sequence by repeated concatenation will have a diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 8a73c52d6e54980..fc3a59e28ce3fee 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -11,8 +11,8 @@ instances of subclasses may have different costs. We use |big O notation|_ to describe how the running time of an operation grows with the size of its input. Unless stated otherwise, *n* denotes the number of -elements currently in the container, and *k* is either the value of a parameter -or the number of elements in the parameter. +elements currently in the container, and *k* is the value of a parameter, the +number of elements in a parameter, or the length of a slice. For a pragmatic approach to assessing time complexity, see Ned Batchelder's `Big-O: How Code Slows as Data Grows `__ @@ -63,6 +63,8 @@ If you need to add or remove at both ends, consider using a - *O*\ (*k*) * - Sort (``s.sort()``) [3]_ - *O*\ (*n* log *n*) + * - Concatenate (``s + t``) + - *O*\ (*n* + *k*) * - Multiply (``s * k``) - *O*\ (*nk*) * - ``x in s`` @@ -116,8 +118,8 @@ to the same value, each of the *O*\ (1) operations below instead takes *O*\ (*n*) time. For more detail on the implementation, see :ref:`how-are-dictionaries-implemented`. -A :class:`frozendict` is immutable, so it does not support setting or deleting -items; the other operations below apply to it at the same costs. +A :class:`frozendict` is immutable, so it does not support setting, deleting, +or updating items; the other operations below apply to it at the same costs. .. list-table:: :header-rows: 1 @@ -134,6 +136,8 @@ items; the other operations below apply to it at the same costs. - *O*\ (1) * - Delete item (``del d[key]``) - *O*\ (1) + * - Update (``d.update(t)``) [1]_ [6]_ + - *O*\ (*k*) * - Iteration [6]_ - *O*\ (*n*) * - Get length (``len(d)``) [4]_ @@ -159,6 +163,8 @@ the same costs. - Complexity * - ``x in s`` - *O*\ (1) + * - Copy (``s.copy()``) [5]_ [6]_ + - *O*\ (*n*) * - Add (``s.add(x)``) [1]_ - *O*\ (1) * - Discard (``s.discard(x)``) @@ -183,11 +189,11 @@ the same costs. =================================================== :class:`str` and :class:`bytes` objects are immutable sequences of characters and -bytes respectively; As with tuples, copying one returns the original object. +bytes, respectively. As with tuples, copying one returns the original object. A :class:`bytearray` is mutable, and additionally supports the mutating operations of :class:`list` (except :meth:`!sort`), at the same costs. However, deleting at -the front only advances the start of the buffer instead of moving the remaining -bytes, and is amortized *O*\ (1). +the front with ``del`` (``del b[0]``, ``del b[:k]``) only advances the start of +the buffer instead of moving the remaining bytes, and is amortized *O*\ (1). .. list-table:: :header-rows: 1 @@ -198,11 +204,11 @@ bytes, and is amortized *O*\ (1). - *O*\ (1) * - Get slice (``s[i:j]``) - *O*\ (*k*) - * - Concatenate (``s + t``) + * - Concatenate (``s + t``) [9]_ - *O*\ (*n* + *k*) * - Multiply (``s * k``) - *O*\ (*nk*) - * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [9]_ + * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [10]_ - *O*\ (*n*) * - Encode or decode - *O*\ (*n*) @@ -252,9 +258,9 @@ A :class:`range` object computes its items on demand from its *start*, *stop* an - *O*\ (1) * - Get slice (``s[i:j]``) - *O*\ (1) - * - ``x in s`` [10]_ + * - ``x in s`` [11]_ - *O*\ (1) - * - Index and count (``s.index(x)``, ``s.count(x)``) [10]_ + * - Index and count (``s.index(x)``, ``s.count(x)``) [11]_ - *O*\ (1) * - Iteration - *O*\ (*n*) @@ -287,7 +293,8 @@ Notes .. [4] The number of elements is stored in the object, so ``len()`` does not need to count them. -.. [5] Copying a :class:`frozendict` is *O*\ (1) as it returns the original object. +.. [5] Copying a :class:`frozendict` or a :class:`frozenset` is *O*\ (1) as it + returns the original object. .. [6] These operations scan the container's internal hash table, which is not shrunk when elements are removed. After removing most elements, they @@ -298,11 +305,16 @@ Notes .. [8] *O*\ (len(*s*) + len(*t*)) if *t* is not a set. -.. [9] A naive substring search would need *O*\ (*nk*) comparisons in the +.. [9] Each concatenation builds a new object, so building a string by + concatenating many pieces in a loop is quadratic in the total length. + See the :ref:`note on concatenating immutable sequences + ` for alternatives. + +.. [10] A naive substring search would need *O*\ (*nk*) comparisons in the worst case, where *k* is the length of the substring searched for, but CPython uses search algorithms with a linear worst case for forward searches. See :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` for details. -.. [10] Assuming :class:`int` or :class:`bool` arguments. For other types, +.. [11] Assuming :class:`int` or :class:`bool` arguments. For other types, the range is searched like any other sequence in *O*\ (*n*) time. From d0ca44cf09cc5e5ede99bd1628dc0e0e7a12154d Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 11 Aug 2026 11:25:31 +0100 Subject: [PATCH 07/17] Simplify complexity notation addressing part of Serhiy's comments --- Doc/library/time-complexity.rst | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index fc3a59e28ce3fee..29e2b331c782459 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -11,8 +11,8 @@ instances of subclasses may have different costs. We use |big O notation|_ to describe how the running time of an operation grows with the size of its input. Unless stated otherwise, *n* denotes the number of -elements currently in the container, and *k* is the value of a parameter, the -number of elements in a parameter, or the length of a slice. +elements currently in the container, and *k* is the value of a numeric +parameter, such as an index or a repeat count. For a pragmatic approach to assessing time complexity, see Ned Batchelder's `Big-O: How Code Slows as Data Grows `__ @@ -54,17 +54,18 @@ If you need to add or remove at both ends, consider using a * - Iteration - *O*\ (*n*) * - Get slice (``s[i:j]``) - - *O*\ (*k*) - * - Set slice (``s[i:j] = t``) - - *O*\ (*k* + *n*) + - *O*\ (*j* - *i*) + * - Set slice (``s[i:j] = t``) [1]_ + - *O*\ (*j* - *i*) if len(*t*) == *j* - *i*, + otherwise *O*\ (*n* - *i* + len(*t*)) * - Delete slice (``del s[i:j]``) - - *O*\ (*n*) + - *O*\ (*n* - *i*) * - Extend (``s.extend(t)``) [1]_ - - *O*\ (*k*) + - *O*\ (len(*t*)) * - Sort (``s.sort()``) [3]_ - *O*\ (*n* log *n*) * - Concatenate (``s + t``) - - *O*\ (*n* + *k*) + - *O*\ (len(*s*) + len(*t*)) * - Multiply (``s * k``) - *O*\ (*nk*) * - ``x in s`` @@ -92,9 +93,9 @@ time as the same object is returned. * - Get item (``s[k]``) - *O*\ (1) * - Get slice (``s[i:j]``) - - *O*\ (*k*) + - *O*\ (*j* - *i*) * - Concatenate (``s + t``) - - *O*\ (*n* + *k*) + - *O*\ (len(*s*) + len(*t*)) * - Multiply (``s * k``) - *O*\ (*nk*) * - Iteration @@ -137,7 +138,7 @@ or updating items; the other operations below apply to it at the same costs. * - Delete item (``del d[key]``) - *O*\ (1) * - Update (``d.update(t)``) [1]_ [6]_ - - *O*\ (*k*) + - *O*\ (len(*t*)) * - Iteration [6]_ - *O*\ (*n*) * - Get length (``len(d)``) [4]_ @@ -203,9 +204,9 @@ the buffer instead of moving the remaining bytes, and is amortized *O*\ (1). * - Get item (``s[k]``) - *O*\ (1) * - Get slice (``s[i:j]``) - - *O*\ (*k*) + - *O*\ (*j* - *i*) * - Concatenate (``s + t``) [9]_ - - *O*\ (*n* + *k*) + - *O*\ (len(*s*) + len(*t*)) * - Multiply (``s * k``) - *O*\ (*nk*) * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [10]_ From 7e297d6722fdb3964ba4cc600fa30ed418427528 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 12 Aug 2026 21:15:03 +0100 Subject: [PATCH 08/17] Carol's suggestion --- Doc/library/time-complexity.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 29e2b331c782459..6b9e302960bd376 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -81,7 +81,7 @@ If you need to add or remove at both ends, consider using a A :class:`tuple` is an :term:`immutable` sequence. Because a tuple can never change, there are no insertion or deletion costs, and making a copy is constant -time as the same object is returned. +time (*O*\ (1)) as the same object is returned. .. list-table:: :header-rows: 1 From 22d5586b18cc4d34f307f6dff37bca1ce66367b2 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 10:43:08 +0100 Subject: [PATCH 09/17] Ned's review Co-authored-by: Ned Batchelder --- Doc/library/time-complexity.rst | 89 ++++++++++++++++----------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 6b9e302960bd376..17e658fbe4bf462 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -4,7 +4,7 @@ Time complexity of operations on built-in types =============================================== -This page documents the time-complexity of various operations on built-in types +This page documents the time complexity of various operations on built-in types in CPython. Other Python implementations may have different performance characteristics. Additionally, the listed costs assume exact built-in types, as instances of subclasses may have different costs. @@ -14,10 +14,6 @@ with the size of its input. Unless stated otherwise, *n* denotes the number of elements currently in the container, and *k* is the value of a numeric parameter, such as an index or a repeat count. -For a pragmatic approach to assessing time complexity, see Ned Batchelder's -`Big-O: How Code Slows as Data Grows `__ -talk and blog post. - .. |big O notation| replace:: Big *O* notation .. _big O notation: https://en.wikipedia.org/wiki/Big_O_notation @@ -37,42 +33,42 @@ If you need to add or remove at both ends, consider using a * - Operation - Complexity - * - Copy (``s.copy()``) + * - Copy (``l.copy()``) - *O*\ (*n*) - * - Append (``s.append(x)``) [1]_ + * - Append (``l.append(x)``) [1]_ - *O*\ (1) - * - Pop (``s.pop(k)``) [1]_ [2]_ + * - Pop (``l.pop(k)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Insert (``s.insert(k, x)``) [1]_ [2]_ + * - Insert (``l.insert(k, x)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Get item (``s[k]``) + * - Get item (``l[k]``) - *O*\ (1) - * - Set item (``s[k] = x``) + * - Set item (``l[k] = x``) - *O*\ (1) - * - Delete item (``del s[k]``) [2]_ + * - Delete item (``del l[k]``) [2]_ - *O*\ (*n* - *k*) * - Iteration - *O*\ (*n*) - * - Get slice (``s[i:j]``) + * - Get slice (``l[i:j]``) - *O*\ (*j* - *i*) - * - Set slice (``s[i:j] = t``) [1]_ + * - Set slice (``l[i:j] = t``) [1]_ - *O*\ (*j* - *i*) if len(*t*) == *j* - *i*, otherwise *O*\ (*n* - *i* + len(*t*)) - * - Delete slice (``del s[i:j]``) + * - Delete slice (``del l[i:j]``) - *O*\ (*n* - *i*) - * - Extend (``s.extend(t)``) [1]_ + * - Extend (``l.extend(t)``) [1]_ - *O*\ (len(*t*)) - * - Sort (``s.sort()``) [3]_ + * - Sort (``l.sort()``) [3]_ - *O*\ (*n* log *n*) - * - Concatenate (``s + t``) - - *O*\ (len(*s*) + len(*t*)) - * - Multiply (``s * k``) + * - Concatenate (``l1 + l2``) + - *O*\ (len(*l1*) + len(*l2*)) + * - Multiply (``l * k``) - *O*\ (*nk*) - * - ``x in s`` + * - ``x in l`` - *O*\ (*n*) - * - ``min(s)``, ``max(s)`` + * - ``min(l)``, ``max(l)`` - *O*\ (*n*) - * - Get length (``len(s)``) [4]_ + * - Get length (``len(l)``) [4]_ - *O*\ (1) @@ -80,8 +76,8 @@ If you need to add or remove at both ends, consider using a =============== A :class:`tuple` is an :term:`immutable` sequence. Because a tuple can never -change, there are no insertion or deletion costs, and making a copy is constant -time (*O*\ (1)) as the same object is returned. +change, there are no insertion or deletion costs, and making a copy simply +returns the same object, so is constant time (*O*\ (1)). .. list-table:: :header-rows: 1 @@ -90,21 +86,21 @@ time (*O*\ (1)) as the same object is returned. - Complexity * - Copy (``tuple(s)``) - *O*\ (1) - * - Get item (``s[k]``) + * - Get item (``t[k]``) - *O*\ (1) - * - Get slice (``s[i:j]``) + * - Get slice (``t[i:j]``) - *O*\ (*j* - *i*) - * - Concatenate (``s + t``) - - *O*\ (len(*s*) + len(*t*)) - * - Multiply (``s * k``) + * - Concatenate (``t1 + t2``) + - *O*\ (len(*t1*) + len(*t2*)) + * - Multiply (``t * k``) - *O*\ (*nk*) * - Iteration - *O*\ (*n*) - * - ``x in s`` + * - ``x in t`` - *O*\ (*n*) - * - ``min(s)``, ``max(s)`` + * - ``min(t)``, ``max(t)`` - *O*\ (*n*) - * - Get length (``len(s)``) [4]_ + * - Get length (``len(t)``) [4]_ - *O*\ (1) @@ -113,14 +109,13 @@ time (*O*\ (1)) as the same object is returned. The times listed for dict objects are average-case times, as they assume the hash function for the objects is sufficiently robust to make collisions -uncommon; they also assume the keys used in parameters are selected uniformly -at random from the set of all keys. In the worst case, when every key hashes -to the same value, each of the *O*\ (1) operations below instead takes -*O*\ (*n*) time. For more detail on the implementation, see -:ref:`how-are-dictionaries-implemented`. +uncommon. They also assume the keys are well-distributed among the set of +possible keys. In the worst case, when every key hashes to the same value, +each of the *O*\ (1) operations below instead takes *O*\ (*n*) time. For more +detail on the implementation, see :ref:`how-are-dictionaries-implemented`. A :class:`frozendict` is immutable, so it does not support setting, deleting, -or updating items; the other operations below apply to it at the same costs. +or updating items. The other operations below apply to it at the same costs. .. list-table:: :header-rows: 1 @@ -151,10 +146,10 @@ or updating items; the other operations below apply to it at the same costs. See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are intentionally very similar, and the same hash collision caveat applies. In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, -and operations that look up every element of an operand degrade accordingly. +and operations that look up every element degrade accordingly. A :class:`frozenset` is :term:`immutable`, so it does not support adding, -discarding, or the in-place update operations; the others below apply to it at +discarding, or the in-place update operations. The others below apply to it at the same costs. .. list-table:: @@ -255,19 +250,19 @@ A :class:`range` object computes its items on demand from its *start*, *stop* an * - Operation - Complexity - * - Get item (``s[k]``) + * - Get item (``r[k]``) - *O*\ (1) - * - Get slice (``s[i:j]``) + * - Get slice (``r[i:j]``) - *O*\ (1) - * - ``x in s`` [11]_ + * - ``x in r`` [11]_ - *O*\ (1) - * - Index and count (``s.index(x)``, ``s.count(x)``) [11]_ + * - Index and count (``r.index(x)``, ``r.count(x)``) [11]_ - *O*\ (1) * - Iteration - *O*\ (*n*) - * - ``min(s)``, ``max(s)`` + * - ``min(r)``, ``max(r)`` - *O*\ (*n*) - * - Get length (``len(s)``) [4]_ + * - Get length (``len(r)``) [4]_ - *O*\ (1) From 5f215109a316472719399f50c50a2fb578347586 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 10:45:25 +0100 Subject: [PATCH 10/17] Missed a few, oops! --- Doc/library/time-complexity.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 17e658fbe4bf462..4964937fdea93fb 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -165,18 +165,18 @@ the same costs. - *O*\ (1) * - Discard (``s.discard(x)``) - *O*\ (1) - * - Union (``s | t``) [6]_ - - *O*\ (len(*s*) + len(*t*)) - * - Intersection (``s & t``, ``s.intersection(t)``) [6]_ [7]_ - - *O*\ (min(len(*s*), len(*t*))) - * - Difference (``s - t``, ``s.difference(t)``) [6]_ [8]_ - - *O*\ (len(*s*)) - * - Difference update (``s.difference_update(t)``) [1]_ [6]_ [7]_ - - *O*\ (min(len(*s*), len(*t*))) - * - Symmetric difference (``s ^ t``) [6]_ - - *O*\ (len(*s*) + len(*t*)) - * - Symmetric difference update (``s.symmetric_difference_update(t)``) [1]_ [6]_ - - *O*\ (len(*t*)) + * - Union (``s1 | s2``) [6]_ + - *O*\ (len(*s1*) + len(*s2*)) + * - Intersection (``s1 & s2``, ``s1.intersection(s2)``) [6]_ [7]_ + - *O*\ (min(len(*s1*), len(*s2*))) + * - Difference (``s1 - s2``, ``s1.difference(s2)``) [6]_ [8]_ + - *O*\ (len(*s1*)) + * - Difference update (``s1.difference_update(s2)``) [1]_ [6]_ [7]_ + - *O*\ (min(len(*s1*), len(*s2*))) + * - Symmetric difference (``s1 ^ s2``) [6]_ + - *O*\ (len(*s1*) + len(*s2*)) + * - Symmetric difference update (``s1.symmetric_difference_update(s2)``) [1]_ [6]_ + - *O*\ (len(*s2*)) * - Get length (``len(s)``) [4]_ - *O*\ (1) From 29e201719115875c4727963f24dddf3f702f0696 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 12:19:29 +0100 Subject: [PATCH 11/17] Ned's review --- Doc/library/time-complexity.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 4964937fdea93fb..2ccbc1eda1c5b07 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -84,7 +84,7 @@ returns the same object, so is constant time (*O*\ (1)). * - Operation - Complexity - * - Copy (``tuple(s)``) + * - Copy (``tuple(t)``) - *O*\ (1) * - Get item (``t[k]``) - *O*\ (1) From 4d223f1dc5c4eede42cff0e0abeccb1bb196cdb2 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 16:41:31 +0100 Subject: [PATCH 12/17] Address @dg-pb's review re. reverse string search complexity Co-authored-by: dgpb <3577712+dg-pb@users.noreply.github.com> --- Doc/library/time-complexity.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 2ccbc1eda1c5b07..aad47d8bbd37966 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -204,8 +204,10 @@ the buffer instead of moving the remaining bytes, and is amortized *O*\ (1). - *O*\ (len(*s*) + len(*t*)) * - Multiply (``s * k``) - *O*\ (*nk*) - * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [10]_ + * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) - *O*\ (*n*) + * - Reverse substring search (``s.rfind(x)``, ``s.rindex(x)``) [10]_ + - *O*\ (*nk*) * - Encode or decode - *O*\ (*n*) * - Iteration @@ -306,11 +308,12 @@ Notes See the :ref:`note on concatenating immutable sequences ` for alternatives. -.. [10] A naive substring search would need *O*\ (*nk*) comparisons in the - worst case, where *k* is the length of the substring searched for, but - CPython uses search algorithms with a linear worst case for forward - searches. See :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` - for details. +.. [10] *k* is the length of the substring searched for. Forward searches use + algorithms with a linear worst case, described in + :source:`Objects/stringlib/stringlib_find_two_way_notes.txt`. Reverse + searches use a simpler algorithm, which is *O*\ (*n*) on typical input but + has no linear worst case. ``s.rpartition(x)`` and ``s.rsplit(x)`` search + backwards too. .. [11] Assuming :class:`int` or :class:`bool` arguments. For other types, the range is searched like any other sequence in *O*\ (*n*) time. From a893080f481cab48cb5423e9475f7180c83fb8bd Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 19:48:33 +0100 Subject: [PATCH 13/17] Try to simplify footnote Co-authored-by: Ned Batchelder Co-authored-by: dgpb <3577712+dg-pb@users.noreply.github.com> --- Doc/library/time-complexity.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index aad47d8bbd37966..7d51e3706a7a18f 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -207,7 +207,7 @@ the buffer instead of moving the remaining bytes, and is amortized *O*\ (1). * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) - *O*\ (*n*) * - Reverse substring search (``s.rfind(x)``, ``s.rindex(x)``) [10]_ - - *O*\ (*nk*) + - *O*\ (*n* × len(*x*)) * - Encode or decode - *O*\ (*n*) * - Iteration @@ -308,12 +308,12 @@ Notes See the :ref:`note on concatenating immutable sequences ` for alternatives. -.. [10] *k* is the length of the substring searched for. Forward searches use - algorithms with a linear worst case, described in - :source:`Objects/stringlib/stringlib_find_two_way_notes.txt`. Reverse - searches use a simpler algorithm, which is *O*\ (*n*) on typical input but - has no linear worst case. ``s.rpartition(x)`` and ``s.rsplit(x)`` search - backwards too. +.. [10] This is the worst case. Reverse searches are *O*\ (*n*) on typical + input. Forward searches instead use a more elaborate algorithm with a + linear worst case, described in + :source:`Objects/stringlib/stringlib_find_two_way_notes.txt`. + ``s.rpartition(x)`` and ``s.rsplit(x)`` search backwards too, with the same + complexity. .. [11] Assuming :class:`int` or :class:`bool` arguments. For other types, the range is searched like any other sequence in *O*\ (*n*) time. From c4a75fd2965414bee17dd484eb5640297a16bd2b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 20 Aug 2026 10:40:29 +0100 Subject: [PATCH 14/17] Remove note --- Doc/library/time-complexity.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 7d51e3706a7a18f..e5e1d93d3426d01 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -312,8 +312,6 @@ Notes input. Forward searches instead use a more elaborate algorithm with a linear worst case, described in :source:`Objects/stringlib/stringlib_find_two_way_notes.txt`. - ``s.rpartition(x)`` and ``s.rsplit(x)`` search backwards too, with the same - complexity. .. [11] Assuming :class:`int` or :class:`bool` arguments. For other types, the range is searched like any other sequence in *O*\ (*n*) time. From eac04b845e5970857fdc1f9aba74ee7772037e93 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 22 Aug 2026 20:40:25 +0100 Subject: [PATCH 15/17] Drop "very" --- Doc/library/time-complexity.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index e5e1d93d3426d01..262ccb8a6e2593f 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -144,7 +144,7 @@ or updating items. The other operations below apply to it at the same costs. ================================== See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are -intentionally very similar, and the same hash collision caveat applies. +similar, and the same hash collision caveat applies. In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, and operations that look up every element degrade accordingly. From 27e9cd6c1251bfaacf6c4a3c32fcadfcfba28904 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 23 Aug 2026 13:41:00 +0100 Subject: [PATCH 16/17] =?UTF-8?q?Address=20B=C3=A9n=C3=A9dikt's=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/time-complexity.rst | 132 ++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index 262ccb8a6e2593f..e7827de781bfd41 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -10,7 +10,7 @@ characteristics. Additionally, the listed costs assume exact built-in types, as instances of subclasses may have different costs. We use |big O notation|_ to describe how the running time of an operation grows -with the size of its input. Unless stated otherwise, *n* denotes the number of +with the size of its inputs. Unless stated otherwise, *n* denotes the number of elements currently in the container, and *k* is the value of a numeric parameter, such as an index or a repeat count. @@ -33,42 +33,42 @@ If you need to add or remove at both ends, consider using a * - Operation - Complexity - * - Copy (``l.copy()``) + * - Copy (``L.copy()``) - *O*\ (*n*) - * - Append (``l.append(x)``) [1]_ + * - Append (``L.append(x)``) [1]_ - *O*\ (1) - * - Pop (``l.pop(k)``) [1]_ [2]_ + * - Pop (``L.pop(k)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Insert (``l.insert(k, x)``) [1]_ [2]_ + * - Insert (``L.insert(k, x)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Get item (``l[k]``) + * - Get item (``L[k]``) - *O*\ (1) - * - Set item (``l[k] = x``) + * - Set item (``L[k] = x``) - *O*\ (1) - * - Delete item (``del l[k]``) [2]_ + * - Delete item (``del L[k]``) [2]_ - *O*\ (*n* - *k*) * - Iteration - *O*\ (*n*) - * - Get slice (``l[i:j]``) + * - Get slice (``L[i:j]``) - *O*\ (*j* - *i*) - * - Set slice (``l[i:j] = t``) [1]_ + * - Set slice (``L[i:j] = t``) [1]_ - *O*\ (*j* - *i*) if len(*t*) == *j* - *i*, otherwise *O*\ (*n* - *i* + len(*t*)) - * - Delete slice (``del l[i:j]``) + * - Delete slice (``del L[i:j]``) - *O*\ (*n* - *i*) - * - Extend (``l.extend(t)``) [1]_ + * - Extend (``L.extend(t)``) [1]_ [3]_ - *O*\ (len(*t*)) - * - Sort (``l.sort()``) [3]_ + * - Sort (``L.sort()``) [4]_ - *O*\ (*n* log *n*) - * - Concatenate (``l1 + l2``) - - *O*\ (len(*l1*) + len(*l2*)) - * - Multiply (``l * k``) + * - Concatenate (``L1 + L2``) + - *O*\ (len(*L1*) + len(*L2*)) + * - Multiply (``L * k``) - *O*\ (*nk*) - * - ``x in l`` + * - ``x in L`` - *O*\ (*n*) - * - ``min(l)``, ``max(l)`` + * - ``min(L)``, ``max(L)`` - *O*\ (*n*) - * - Get length (``len(l)``) [4]_ + * - Get length (``len(L)``) [5]_ - *O*\ (1) @@ -100,7 +100,7 @@ returns the same object, so is constant time (*O*\ (1)). - *O*\ (*n*) * - ``min(t)``, ``max(t)`` - *O*\ (*n*) - * - Get length (``len(t)``) [4]_ + * - Get length (``len(t)``) [5]_ - *O*\ (1) @@ -111,8 +111,9 @@ The times listed for dict objects are average-case times, as they assume the hash function for the objects is sufficiently robust to make collisions uncommon. They also assume the keys are well-distributed among the set of possible keys. In the worst case, when every key hashes to the same value, -each of the *O*\ (1) operations below instead takes *O*\ (*n*) time. For more -detail on the implementation, see :ref:`how-are-dictionaries-implemented`. +each of the *O*\ (1) operations below instead takes *O*\ (*n*) time. They also +assume that hashing and comparing a key is *O*\ (1). For more detail on the +implementation, see :ref:`how-are-dictionaries-implemented`. A :class:`frozendict` is immutable, so it does not support setting, deleting, or updating items. The other operations below apply to it at the same costs. @@ -124,19 +125,19 @@ or updating items. The other operations below apply to it at the same costs. - Complexity * - ``key in d`` - *O*\ (1) - * - Copy (``d.copy()``) [5]_ [6]_ + * - Copy (``d.copy()``) [6]_ [7]_ - *O*\ (*n*) - * - Get item (``d[key]``) + * - Get item (``d[key]``, ``d.get(key)``) - *O*\ (1) * - Set item (``d[key] = value``) [1]_ - *O*\ (1) - * - Delete item (``del d[key]``) + * - Delete item (``del d[key]``, ``d.pop(key)``) - *O*\ (1) - * - Update (``d.update(t)``) [1]_ [6]_ + * - Update (``d.update(t)``, ``d |= t``) [1]_ [3]_ [7]_ - *O*\ (len(*t*)) - * - Iteration [6]_ + * - Iteration [7]_ - *O*\ (*n*) - * - Get length (``len(d)``) [4]_ + * - Get length (``len(d)``) [5]_ - *O*\ (1) @@ -144,7 +145,7 @@ or updating items. The other operations below apply to it at the same costs. ================================== See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are -similar, and the same hash collision caveat applies. +similar, and the same caveats apply. In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, and operations that look up every element degrade accordingly. @@ -159,25 +160,29 @@ the same costs. - Complexity * - ``x in s`` - *O*\ (1) - * - Copy (``s.copy()``) [5]_ [6]_ + * - Copy (``s.copy()``) [6]_ [7]_ - *O*\ (*n*) * - Add (``s.add(x)``) [1]_ - *O*\ (1) - * - Discard (``s.discard(x)``) + * - Discard (``s.discard(x)``, ``s.remove(x)``) - *O*\ (1) - * - Union (``s1 | s2``) [6]_ + * - Union (``s1 | s2``, ``s1.union(s2)``) [7]_ - *O*\ (len(*s1*) + len(*s2*)) - * - Intersection (``s1 & s2``, ``s1.intersection(s2)``) [6]_ [7]_ + * - Update (``s1 |= s2``, ``s1.update(s2)``) [1]_ [7]_ + - *O*\ (len(*s2*)) + * - Intersection (``s1 & s2``, ``s1.intersection(s2)``) [7]_ [8]_ + - *O*\ (min(len(*s1*), len(*s2*))) + * - Intersection update (``s1 &= s2``, ``s1.intersection_update(s2)``) [1]_ [7]_ [8]_ - *O*\ (min(len(*s1*), len(*s2*))) - * - Difference (``s1 - s2``, ``s1.difference(s2)``) [6]_ [8]_ + * - Difference (``s1 - s2``, ``s1.difference(s2)``) [7]_ [9]_ - *O*\ (len(*s1*)) - * - Difference update (``s1.difference_update(s2)``) [1]_ [6]_ [7]_ + * - Difference update (``s1 -= s2``, ``s1.difference_update(s2)``) [1]_ [7]_ [8]_ - *O*\ (min(len(*s1*), len(*s2*))) - * - Symmetric difference (``s1 ^ s2``) [6]_ + * - Symmetric difference (``s1 ^ s2``, ``s1.symmetric_difference(s2)``) [7]_ - *O*\ (len(*s1*) + len(*s2*)) - * - Symmetric difference update (``s1.symmetric_difference_update(s2)``) [1]_ [6]_ + * - Symmetric difference update (``s1 ^= s2``, ``s1.symmetric_difference_update(s2)``) [1]_ [7]_ - *O*\ (len(*s2*)) - * - Get length (``len(s)``) [4]_ + * - Get length (``len(s)``) [5]_ - *O*\ (1) @@ -200,19 +205,19 @@ the buffer instead of moving the remaining bytes, and is amortized *O*\ (1). - *O*\ (1) * - Get slice (``s[i:j]``) - *O*\ (*j* - *i*) - * - Concatenate (``s + t``) [9]_ + * - Concatenate (``s + t``) [10]_ - *O*\ (len(*s*) + len(*t*)) * - Multiply (``s * k``) - *O*\ (*nk*) - * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) + * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [11]_ - *O*\ (*n*) - * - Reverse substring search (``s.rfind(x)``, ``s.rindex(x)``) [10]_ + * - Reverse substring search (``s.rfind(x)``, ``s.rindex(x)``) [11]_ [12]_ - *O*\ (*n* × len(*x*)) - * - Encode or decode + * - Encode or decode [13]_ - *O*\ (*n*) * - Iteration - *O*\ (*n*) - * - Get length (``len(s)``) [4]_ + * - Get length (``len(s)``) [5]_ - *O*\ (1) @@ -235,9 +240,13 @@ buffer. - *O*\ (1) * - Get slice (``v[i:j]``) - *O*\ (1) + * - Index (``v.index(x)``) [11]_ [14]_ + - *O*\ (*n*) + * - Count (``v.count(x)``) [14]_ + - *O*\ (*n*) * - Convert to bytes (``v.tobytes()``, ``bytes(v)``) - *O*\ (*n*) - * - Get length (``len(v)``) [4]_ + * - Get length (``len(v)``) [5]_ - *O*\ (1) @@ -256,15 +265,15 @@ A :class:`range` object computes its items on demand from its *start*, *stop* an - *O*\ (1) * - Get slice (``r[i:j]``) - *O*\ (1) - * - ``x in r`` [11]_ + * - ``x in r`` [15]_ - *O*\ (1) - * - Index and count (``r.index(x)``, ``r.count(x)``) [11]_ + * - Index and count (``r.index(x)``, ``r.count(x)``) [15]_ - *O*\ (1) * - Iteration - *O*\ (*n*) * - ``min(r)``, ``max(r)`` - *O*\ (*n*) - * - Get length (``len(r)``) [4]_ + * - Get length (``len(r)``) [5]_ - *O*\ (1) @@ -284,34 +293,45 @@ Notes operations; and operating at the end of the list moves nothing and is *O*\ (1). -.. [3] This is the worst case scenario. Sorting is adaptive and input that is +.. [3] Plus the cost of iterating over *t*, which may be expensive for an + arbitrary iterable. + +.. [4] This is the worst case scenario. Sorting is adaptive and input that is already sorted or reverse-sorted takes only *O*\ (*n*) comparisons. See :source:`Objects/listsort.txt` for more information. -.. [4] The number of elements is stored in the object, so ``len()`` does +.. [5] The number of elements is stored in the object, so ``len()`` does not need to count them. -.. [5] Copying a :class:`frozendict` or a :class:`frozenset` is *O*\ (1) as it +.. [6] Copying a :class:`frozendict` or a :class:`frozenset` is *O*\ (1) as it returns the original object. -.. [6] These operations scan the container's internal hash table, which is +.. [7] These operations scan the container's internal hash table, which is not shrunk when elements are removed. After removing most elements, they still take time proportional to the container's former size, until a later insertion triggers a resize. -.. [7] *O*\ (len(*t*)) if *t* is not a set. +.. [8] *O*\ (len(*t*)) if *t* is not a set. -.. [8] *O*\ (len(*s*) + len(*t*)) if *t* is not a set. +.. [9] *O*\ (len(*s*) + len(*t*)) if *t* is not a set. -.. [9] Each concatenation builds a new object, so building a string by +.. [10] Each concatenation builds a new object, so building a string by concatenating many pieces in a loop is quadratic in the total length. See the :ref:`note on concatenating immutable sequences ` for alternatives. -.. [10] This is the worst case. Reverse searches are *O*\ (*n*) on typical +.. [11] With *start* and *end* arguments, *n* is the length of the region + searched rather than of *s*, and unlike slicing nothing is copied. + +.. [12] This is the worst case. Reverse searches are *O*\ (*n*) on typical input. Forward searches instead use a more elaborate algorithm with a linear worst case, described in :source:`Objects/stringlib/stringlib_find_two_way_notes.txt`. -.. [11] Assuming :class:`int` or :class:`bool` arguments. For other types, +.. [13] This assumes a codec that does a constant amount of work per character. + +.. [14] These unpack and compare each element individually, so they are much + slower than the equivalent :class:`bytes` methods. + +.. [15] Assuming :class:`int` or :class:`bool` arguments. For other types, the range is searched like any other sequence in *O*\ (*n*) time. From 116f21ec9f82125eeaa19453544e43bdd4716eb9 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 23 Aug 2026 18:39:31 +0100 Subject: [PATCH 17/17] Switch back to `l` --- Doc/library/time-complexity.rst | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst index e7827de781bfd41..5ce02ac2761b621 100644 --- a/Doc/library/time-complexity.rst +++ b/Doc/library/time-complexity.rst @@ -33,42 +33,42 @@ If you need to add or remove at both ends, consider using a * - Operation - Complexity - * - Copy (``L.copy()``) + * - Copy (``l.copy()``) - *O*\ (*n*) - * - Append (``L.append(x)``) [1]_ + * - Append (``l.append(x)``) [1]_ - *O*\ (1) - * - Pop (``L.pop(k)``) [1]_ [2]_ + * - Pop (``l.pop(k)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Insert (``L.insert(k, x)``) [1]_ [2]_ + * - Insert (``l.insert(k, x)``) [1]_ [2]_ - *O*\ (*n* - *k*) - * - Get item (``L[k]``) + * - Get item (``l[k]``) - *O*\ (1) - * - Set item (``L[k] = x``) + * - Set item (``l[k] = x``) - *O*\ (1) - * - Delete item (``del L[k]``) [2]_ + * - Delete item (``del l[k]``) [2]_ - *O*\ (*n* - *k*) * - Iteration - *O*\ (*n*) - * - Get slice (``L[i:j]``) + * - Get slice (``l[i:j]``) - *O*\ (*j* - *i*) - * - Set slice (``L[i:j] = t``) [1]_ + * - Set slice (``l[i:j] = t``) [1]_ - *O*\ (*j* - *i*) if len(*t*) == *j* - *i*, otherwise *O*\ (*n* - *i* + len(*t*)) - * - Delete slice (``del L[i:j]``) + * - Delete slice (``del l[i:j]``) - *O*\ (*n* - *i*) - * - Extend (``L.extend(t)``) [1]_ [3]_ + * - Extend (``l.extend(t)``) [1]_ [3]_ - *O*\ (len(*t*)) - * - Sort (``L.sort()``) [4]_ + * - Sort (``l.sort()``) [4]_ - *O*\ (*n* log *n*) - * - Concatenate (``L1 + L2``) - - *O*\ (len(*L1*) + len(*L2*)) - * - Multiply (``L * k``) + * - Concatenate (``l1 + l2``) + - *O*\ (len(*l1*) + len(*l2*)) + * - Multiply (``l * k``) - *O*\ (*nk*) - * - ``x in L`` + * - ``x in l`` - *O*\ (*n*) - * - ``min(L)``, ``max(L)`` + * - ``min(l)``, ``max(l)`` - *O*\ (*n*) - * - Get length (``len(L)``) [5]_ + * - Get length (``len(l)``) [5]_ - *O*\ (1)