From 80088bf411a9d86b09fec88b49bc01a323e03b70 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 8 Jul 2026 21:42:02 +0200 Subject: [PATCH 01/13] 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 c914089e9806ec..02a3a3a22070da 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 c2f8f72ee1f2c4..4e1157e6ebe729 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 bb00a4f02f0efd..02f89b419d9fc2 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 d09a6c92bbd37d..65128149de33ec 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 8fc77be520d426..f28c03e2fae092 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 886648e820f071..b2f0d934980353 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 00000000000000..ad9101f2c1524a --- /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 276e31a3056f0e..77dd8a6e66d5cc 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/13] 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 ad9101f2c1524a..538f10ffdee734 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/13] 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 65128149de33ec..d09a6c92bbd37d 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/13] 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 538f10ffdee734..1fef65d476363e 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/13] =?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 1fef65d476363e..8a73c52d6e5498 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/13] 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 b2f0d934980353..5df1bae984c572 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 8a73c52d6e5498..fc3a59e28ce3fe 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/13] 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 fc3a59e28ce3fe..29e2b331c78245 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/13] 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 29e2b331c78245..6b9e302960bd37 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/13] 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 6b9e302960bd37..17e658fbe4bf46 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/13] 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 17e658fbe4bf46..4964937fdea93f 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/13] 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 4964937fdea93f..2ccbc1eda1c5b0 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/13] 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 2ccbc1eda1c5b0..aad47d8bbd3796 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/13] 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 aad47d8bbd3796..7d51e3706a7a18 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.