Python’s bisect module solves a focused problem: finding the correct position for a value inside an already sorted list. Instead of scanning every element, it uses binary search and discards half of the remaining range after each comparison. This makes it useful for rankings, price bands, thresholds, schedules, ordered indexes, and read-heavy in-memory collections.
This guide explains bisect_left, bisect_right, insort_left, and insort_right, including their real performance costs, duplicate handling, key functions, common mistakes, and practical design patterns.
Why binary search matters
A linear scan may inspect many elements. Binary search needs far fewer comparisons because the search interval shrinks exponentially. Finding an insertion point is logarithmic. Inserting into a Python list is still linear, however, because all following references may need to move.
For that reason, bisect works best when queries are frequent and insertions are relatively rare. A database index, balanced tree, heap, or specialized sorted collection may be a better choice for highly mutable workloads.
bisect_left and bisect_right
from bisect import bisect_left, bisect_right
values = [10, 20, 20, 20, 30, 40]
start = bisect_left(values, 20)
end = bisect_right(values, 20)
print(start) # 1
print(end) # 4
print(values[start:end])
bisect_left returns the first valid position for the target. bisect_right returns the position immediately after all equal values. Together they can count duplicates, extract equal ranges, and define whether new duplicates should be inserted before or after existing ones.
Insert while preserving order
from bisect import insort_left, insort_right
scores = [5.0, 6.5, 8.0, 9.5]
insort_left(scores, 8.0)
insort_right(scores, 8.0)
print(scores)
The insort helpers combine position lookup and list insertion. They are clearer than manually calling a search function and then list.insert. The lookup is fast, but element movement still dominates the insertion cost.
Find an exact value safely
from bisect import bisect_left
def find_exact(items, target):
position = bisect_left(items, target)
if position != len(items) and items[position] == target:
return position
return -1
The returned index is only an insertion point. It does not prove that the item exists. Always check the boundary and compare the value before treating the lookup as a successful match.
Build threshold tables
from bisect import bisect_right
limits = [100, 250, 500, 1000]
labels = ["tiny", "small", "medium", "large", "enterprise"]
def classify(value):
return labels[bisect_right(limits, value)]
This pattern converts ordered boundaries into a fast classification table. It is useful for shipping tiers, score bands, pricing levels, latency categories, tax brackets, and service-level rules.
Use the key parameter
Modern Python versions allow a key function for extracting comparison keys from stored elements. The key function is applied to list elements, not to the search value, so pass a compatible key as x.
from bisect import bisect_left
products = [
{"name": "A", "price": 10},
{"name": "B", "price": 25},
{"name": "C", "price": 40},
]
position = bisect_left(products, 30, key=lambda item: item["price"])
print(position)
Repeated searches may call the key function many times. If computing the key is expensive, keep a parallel list of precomputed keys or cache the calculation.
Parallel key lists
from bisect import bisect_left
records = [
{"id": 101, "name": "Ana"},
{"id": 205, "name": "Bruno"},
{"id": 330, "name": "Carla"},
]
ids = [record["id"] for record in records]
position = bisect_left(ids, 205)
if position < len(ids) and ids[position] == 205:
print(records[position])
This design avoids repeated key extraction. Its main risk is synchronization: insertions, removals, and updates must modify both lists at matching positions. Encapsulating the lists in a class makes that invariant easier to protect.
Common mistakes
The first mistake is using bisect on unsorted data. The module does not validate ordering and can return an index that looks reasonable but is wrong. The second mistake is assuming insertion is logarithmic. Search is logarithmic; list mutation is linear. The third mistake is treating the insertion point as proof of existence. The fourth is sorting by one criterion and searching by another.
Concurrency creates another risk. A separate search followed by insertion should not be assumed atomic when multiple threads or processes mutate the same collection. Protect the operation with appropriate synchronization or move the data to a transactional store.
When bisect is a good fit
Use it for small and medium in-memory tables, mostly static thresholds, sorted configuration values, read-heavy catalogs, time boundaries, and lightweight indexes. Avoid using it as a replacement for a database index in large, frequently changing datasets or as a priority queue with constant removals.
Complete example: ordered ranking
from bisect import bisect_left, bisect_right
ranking = []
points = []
def add_player(name, score):
position = bisect_right(points, score)
points.insert(position, score)
ranking.insert(position, {"name": name, "score": score})
def players_above(minimum):
position = bisect_left(points, minimum)
return ranking[position:]
The example keeps the scoring keys separate from the records. This improves repeated lookup performance, but both lists must remain synchronized. Production code should hide them behind a class and include tests for empty input, duplicates, boundary scores, removals, and updates.
Practical recommendations
Document the ordering rule, validate incoming values, test duplicates and boundaries, benchmark insertion-heavy workloads, and encapsulate data invariants. Related Academify guides include difflib, fractions, statistics, types, and linecache.
For authoritative details, see the official bisect documentation and the Python data structures tutorial.
Conclusion
bisect is compact, predictable, and efficient for finding positions in ordered lists. Its greatest advantage is fast lookup with very little code. When the workload consists of many reads and comparatively few writes, it is often an excellent standard-library solution. When updates dominate, measure the real cost and consider a more specialized structure.







