A sorted list is useful when a program needs ordered iteration and frequent searches but does not require the repeated minimum extraction of a priority queue. Python’s bisect module provides binary-search operations for this exact representation.
The module does not create a special container. It works with an existing sorted sequence and finds the position where a value belongs. That makes it small and predictable, but it also means the caller is responsible for preserving sorted order and understanding that inserting into a Python list still requires moving elements.
Search for an insertion point
bisect_left() returns the first position where a value can be inserted while preserving ascending order:
from bisect import bisect_left
values = [10, 20, 20, 40]
position = bisect_left(values, 20)
print(position)The result is 1, the position before the existing 20 values.
This is slightly different from a search API that returns a Boolean or an element. bisect_left() always returns an insertion index in the range from 0 through len(values).
That property is useful even when the target is absent:
from bisect import bisect_left
values = [10, 20, 40]
position = bisect_left(values, 30)
print(position)The result is 2, which is where 30 belongs.
Choose left or right behavior for duplicates
When equal values already exist, the two main search functions define opposite boundaries.
from bisect import bisect_left, bisect_right
values = [10, 20, 20, 20, 40]
print(bisect_left(values, 20))
print(bisect_right(values, 20))bisect_left() returns the position before the equal run. bisect_right() returns the position after it. bisect() is an alias for bisect_right().
This gives a convenient way to find the range containing all occurrences of a value:
from bisect import bisect_left, bisect_right
values = [10, 20, 20, 20, 40]
start = bisect_left(values, 20)
stop = bisect_right(values, 20)
print(values[start:stop])The slice contains the three matching values.
Check whether an exact value exists
Because a bisection operation returns a position rather than a match result, verify the candidate before indexing it:
from bisect import bisect_left
def contains(sorted_values, target):
position = bisect_left(sorted_values, target)
return position != len(sorted_values) and sorted_values[position] == targetThe bounds check matters. If target belongs after every existing element, the returned index equals the list length.
Insert while preserving order
insort_left() combines a bisection search with list insertion:
from bisect import insort_left
values = [10, 20, 40]
insort_left(values, 30)
print(values)The result is:
[10, 20, 30, 40]insort_right() uses the right boundary for equal values. insort() is an alias for insort_right().
These functions mutate the list in place. They do not return a new sorted list.
Binary search does not make insertion logarithmic
The search for an insertion point takes logarithmic time, but inserting into the middle of a Python list is linear because later references must be shifted.
This distinction is central to choosing bisect correctly. It is attractive when searches are common and insertions are modest, or when the collection is small enough that list movement is inexpensive. It is less attractive for a very large structure receiving frequent arbitrary insertions.
For a workload that repeatedly inserts and removes the smallest item, a heap may be a better representation. For a workload that needs many arbitrary ordered insertions at large scale, a tree-based or database-backed structure may be more appropriate.
Search records with a key function
The bisection functions accept a key parameter for sequences whose ordering is based on part of each element.
from bisect import bisect_left
records = [
("low", 10),
("medium", 20),
("high", 40),
]
position = bisect_left(records, 25, key=lambda record: record[1])
print(position)The result is 2 because a key value of 25 belongs between 20 and 40.
An important detail is that the key function is applied to sequence elements during the search, but not to the search value x. The caller therefore passes 25, not a record whose key happens to be 25.
Insertion applies the key differently
For insort_left() and insort_right(), the key function is used to find the insertion point for the new element, but the original element itself is inserted into the list.
from bisect import insort_left
records = [
("low", 10),
("high", 40),
]
insort_left(records, ("medium", 20), key=lambda record: record[1])The resulting list remains ordered by the numeric field while retaining complete tuples.
Keep this distinction in mind when sharing a key function between direct searches and insertion operations: a direct bisect_left() search receives a key value as x, while insort_left() receives the full element to insert.
Avoid recomputing expensive keys
The bisection search functions discard key-function results after use. Repeated searches can therefore call an expensive key function many times.
If key extraction is costly, one option is to maintain a parallel list of precomputed keys:
from bisect import bisect_left
names = ["Ada", "Grace", "Margaret"]
lengths = [len(name) for name in names]
position = bisect_left(lengths, 6)
print(position)A parallel-key design is only correct if both lists are updated together. Another option for pure key functions is caching, but cache lifetime and invalidation should be considered before adding that complexity.
For cheap attribute or tuple access, recomputation is usually simpler than maintaining duplicate state.
Restrict a search to a subrange
The optional lo and hi arguments limit the portion of the sequence considered:
from bisect import bisect_left
values = [5, 10, 20, 30, 40, 50]
position = bisect_left(values, 25, lo=2, hi=5)
print(position)The search examines the half-open range from index 2 through index 5, excluding 5 itself.
The returned position is still an index into the original sequence, not an offset relative to lo.
Use subranges when the program already knows that only part of a sorted sequence can contain the relevant insertion point. Do not use them to search an arbitrarily unsorted subsection; the searched range still needs the ordering expected by the bisection operation.
Use bisection for threshold lookup
Insertion is not the only use case. A boundary search can map a numeric value to a sorted set of thresholds.
from bisect import bisect_right
thresholds = [0, 50, 80]
labels = ["low", "medium", "high"]
def classify(score):
position = bisect_right(thresholds, score) - 1
if position < 0:
raise ValueError("score is below the supported range")
return labels[position]
print(classify(67))The result is medium.
This pattern works because the thresholds are sorted and each threshold marks the start of a range. Boundary conditions should be explicit: decide whether equality belongs to the lower or upper interval, then choose bisect_left() or bisect_right() accordingly.
Preserve the ordering invariant
Bisection assumes the searched sequence is already sorted according to the same ordering used by the operation.
This code creates a subtle bug:
values = [10, 20, 30]
values.append(5)A later call to bisect_left(values, 15) is no longer operating on valid input. The module does not sort the list or validate the invariant first.
Treat direct list mutation as part of the data structure’s contract. If callers can append or replace arbitrary elements, hide the list behind a small API that centralizes insertion and update behavior.
Updates can require removal and reinsertion
If an object’s sort key changes while it is already stored, its current position may become invalid.
Do not simply mutate the key-bearing field and continue searching the list. Remove the element and reinsert it at the correct position, or rebuild the ordering after a batch of changes.
For mutable records that change frequently, a sorted list may be the wrong representation altogether.
Do not share a changing list across threads without coordination
The bisect documentation warns that its functions are not thread-safe when multiple threads operate concurrently on the same sequence. Concurrent mutation can invalidate the ordering assumptions used by another operation.
If multiple threads share a sorted list, protect the complete read-modify-write operation with appropriate synchronization. Locking only the insertion statement is not enough when a search result can become stale before the insertion happens.
A single-threaded event loop or an ownership model where only one worker mutates the collection can avoid this class of race without adding a lock around every operation.
Compare bisect with nearby tools
Several standard operations solve related but different problems.
Use bisect when a list stays sorted over time and the program needs insertion points or boundary searches.
Use sorted() when all data can be collected first and the primary task is to produce a completely ordered result once.
Use heapq when the important operation is repeatedly obtaining the smallest pending item while full ordering is unnecessary.
Use a dictionary or set when exact membership lookup matters but ordering does not.
The representation should follow the access pattern rather than the fact that binary search sounds efficient.
Common pitfalls
Searching an unsorted list
The functions assume sorted input. Results on data that violates that invariant are not meaningful for maintaining sorted order.
Forgetting the end position
A returned insertion index can equal len(sequence). Check bounds before using it as an existing-element index.
Expecting cheap insertion
Binary search finds the position efficiently; Python list insertion still moves later elements.
Passing a full record to a keyed search
With bisect_left(records, x, key=...), x is compared with keys extracted from the records. Pass the comparable key value. insort_left(), by contrast, receives the full element to insert.
Mutating a sort key in place
Changing an element’s ordering key can invalidate the sequence. Reposition the element or choose a representation designed for frequent updates.
Assuming bisection checks equality
The search functions locate boundaries using ordering comparisons. If exact membership matters, inspect the candidate element after finding the insertion point.
Choose sorted lists for the right workload
bisect is most useful when a plain Python list is already a good storage representation and the program needs faster boundary searches without introducing a heavier data structure.
Its trade-off is straightforward: searching is logarithmic, but insertion remains linear. In exchange, the program gets compact storage, efficient indexed access, natural ordered iteration, and a standard-library API with explicit duplicate-boundary behavior.
Keep the sorted invariant visible, choose left or right semantics deliberately, and treat key changes and concurrent mutation as structural operations rather than ordinary field updates. With those rules in place, bisect is a precise tool for maintaining modest sorted collections and implementing range-oriented searches.