If you've ever needed to keep a Python list sorted while inserting new values on the fly, you've probably felt the pain of calling sorted() over and over. The python bisect module fixes that. It's a built-in standard library tool that uses binary search to find the exact insertion point in a sorted list in O(log n) time — no full re-sort needed. Whether you're tracking scores, processing real-time data, or building a lookup table, bisect makes sorted list operations clean and efficient. There's nothing to install — just import it.
import bisect
That's all it takes to unlock one of Python's most underrated utilities. Let's break down every function it offers, see how each one behaves, and build something real with it.
bisect.bisect_left() is the first function most people learn from the bisect module, and for good reason. It takes a sorted list and a target value, then returns the index where that value should be inserted to keep the list sorted — specifically, the leftmost valid position. If the value already exists in the list, it returns the index of that first occurrence, not after it.
Picture a sorted row of tiles numbered 1 through 10. You want to add a tile labeled 5. bisect_left points you to the slot right before the existing 5 — the leftmost spot where 5 could live.
import bisect
scores = [10, 20, 30, 40, 50]
position = bisect.bisect_left(scores, 30)
print(position)
Output:
2
The value 30 sits at index 2 in the list. bisect_left returns 2 because the leftmost valid position for inserting 30 is exactly where the existing 30 already is. This behavior is incredibly useful for checking if an element already exists — if scores[position] == 30, it's there; if not, that's where it would go.
You can also pass optional lo and hi arguments to restrict the search to a subrange of the list without slicing:
import bisect
prices = [5, 15, 25, 35, 45, 55, 65]
position = bisect.bisect_left(prices, 35, 2, 5)
print(position)
Output:
3
By passing lo=2 and hi=5, the search only considers indices 2 through 4 — no new list is created, so this stays memory-efficient.
bisect.bisect_right() is nearly identical to bisect_left, but with one critical difference in how it handles duplicate values. When the target value already exists in the list, bisect_right returns the position after all of its occurrences — the rightmost valid slot. bisect.bisect() is an alias for bisect_right, so the two are interchangeable.
Using the tile analogy again: if tile 5 already exists, bisect_right says "put the new tile right after the existing one."
import bisect
scores = [10, 20, 30, 30, 40, 50]
left = bisect.bisect_left(scores, 30)
right = bisect.bisect_right(scores, 30)
print("bisect_left:", left)
print("bisect_right:", right)
Output:
bisect_left: 2
bisect_right: 4
The list has 30 at both index 2 and index 3. bisect_left returns 2 (before the first one), and bisect_right returns 4 (after the last one). That two-index gap tells you everything — there are two copies of 30 in the list, and they live between positions 2 and 4.
bisect.bisect() is just the short form of bisect_right():
import bisect
temperatures = [18, 21, 23, 23, 27, 30]
print(bisect.bisect(temperatures, 23))
print(bisect.bisect_right(temperatures, 23))
Output:
4
4
Knowing when to use bisect_left versus bisect_right comes down to one question: when you have duplicates, do you want to insert before or after them? For most use cases, bisect_right is the safer default because it naturally preserves insertion order among equal elements.
Finding the insertion point is useful on its own, but you usually want to actually place the value there too. That's exactly what bisect.insort() and its variants do. insort_right() (aliased as insort()) finds the right position and inserts the value in one call — your list stays sorted automatically, no extra steps.
import bisect
leaderboard = [100, 250, 380, 490, 610]
bisect.insort(leaderboard, 320)
print(leaderboard)
Output:
[100, 250, 320, 380, 490, 610]
320 landed cleanly between 250 and 380. The list is still sorted. You didn't have to call sort(), slice anything, or figure out the index manually — insort handled all of it.
Both insort_left and insort_right are available when you care about where duplicates land:
import bisect
values = [1, 3, 5, 5, 7, 9]
left_version = values[:]
bisect.insort_left(left_version, 5)
print("insort_left: ", left_version)
right_version = values[:]
bisect.insort_right(right_version, 5)
print("insort_right:", right_version)
Output:
insort_left: [1, 3, 5, 5, 5, 7, 9]
insort_right: [1, 3, 5, 5, 5, 7, 9]
The final lists look the same because the values are equal, but the new 5 was inserted at index 2 in the left version and index 4 in the right version. If your list holds objects with meaningful ordering — say, timestamped events with equal priorities — this distinction directly controls which one appears first after equal elements.
One of the most elegant and underused tricks with python bisect is using bisect_right as a fast lookup against a sorted set of breakpoints. Instead of writing a messy chain of if-elif conditions, you define a list of boundary values and a corresponding list of results, then let bisect do the indexing for you.
Here's a classic grade calculator built this way:
import bisect
def get_grade(score):
breakpoints = [60, 70, 80, 90]
grades = ['F', 'D', 'C', 'B', 'A']
index = bisect.bisect_right(breakpoints, score)
return grades[index]
test_scores = [45, 62, 71, 85, 93, 100]
for score in test_scores:
print(f"Score {score} -> Grade {get_grade(score)}")
Output:
Score 45 -> Grade F
Score 62 -> Grade D
Score 71 -> Grade C
Score 85 -> Grade B
Score 93 -> Grade A
Score 100 -> Grade A
bisect_right tells you which bracket the score falls into. A score of 62 sits to the right of the 60 breakpoint but left of 70, so it gets index 1, which maps to 'D'. This pattern generalizes to tax brackets, pricing tiers, shipping zones — anywhere you need to map a number to a category. Adding a new tier means appending one value to each list, nothing more.
The python bisect module really shines when you're processing values one at a time and need the list to stay sorted after every insertion. This is a common pattern in event processing, leaderboard management, and any scenario where you're collecting data incrementally and querying it in between.
import bisect
readings = []
incoming = [42, 17, 88, 55, 23, 91, 37]
for value in incoming:
bisect.insort(readings, value)
print(f"Inserted {value:>3} -> {readings}")
Output:
Inserted 42 -> [42]
Inserted 17 -> [17, 42]
Inserted 88 -> [17, 42, 88]
Inserted 55 -> [17, 42, 55, 88]
Inserted 23 -> [17, 23, 42, 55, 88]
Inserted 91 -> [17, 23, 42, 55, 88, 91]
Inserted 37 -> [17, 23, 37, 42, 55, 88, 91]
Every step adds a new value and the list immediately reflects the correct sorted order. You never call sort(). You never scan the list to find the right spot manually. Bisect handles all of that. For long-running processes where new items arrive continuously, this approach is far leaner than sorting from scratch each time.
You can also combine bisect_left and bisect_right to count how many times a value appears, or to extract all values within a range — both without any loops:
import bisect
data = [2, 5, 7, 7, 7, 10, 14, 19]
# Count occurrences of 7
count = bisect.bisect_right(data, 7) - bisect.bisect_left(data, 7)
print(f"Count of 7s: {count}")
# Find indices of all values between 6 and 12
lo = bisect.bisect_left(data, 6)
hi = bisect.bisect_right(data, 12)
print(f"Values in [6, 12]: {data[lo:hi]}")
Output:
Count of 7s: 3
Values in [6, 12]: [7, 7, 7, 10]
The official Python documentation for the bisect module covers additional edge cases and notes about the internal C implementation, which is worth a read once you're comfortable with the basics.
This example ties together every major feature of the python bisect module in a student score tracking system. It maintains a live sorted leaderboard using insort, detects duplicate scores with bisect_left, assigns letter grades with a bisect_right lookup table, calculates each student's rank, and counts how many students scored below each new entry.
import bisect
def assign_grade(score):
breakpoints = [50, 60, 70, 80, 90]
grades = ['F', 'E', 'D', 'C', 'B', 'A']
return grades[bisect.bisect_right(breakpoints, score)]
def score_exists(leaderboard, score):
pos = bisect.bisect_left(leaderboard, score)
return pos < len(leaderboard) and leaderboard[pos] == score
def students_below(leaderboard, score):
return bisect.bisect_left(leaderboard, score)
leaderboard = []
student_scores = [72, 88, 55, 91, 72, 63, 47, 88, 76, 95]
print("=== Student Score Tracker ===
")
for i, score in enumerate(student_scores, start=1):
if score_exists(leaderboard, score):
print(f"Student {i:>2}: Score {score} (duplicate — tie detected)")
else:
print(f"Student {i:>2}: Score {score} (new entry)")
bisect.insort(leaderboard, score)
grade = assign_grade(score)
rank = len(leaderboard) - bisect.bisect_right(leaderboard, score) + 1
below = students_below(leaderboard, score)
print(f" Grade: {grade} | Rank: #{rank} | Students below: {below}")
print(f" Leaderboard: {leaderboard}
")
print("=== Final Leaderboard ===")
print(leaderboard)
print("
=== Grade Distribution ===")
for score in leaderboard:
print(f" Score {score}: {assign_grade(score)}")
Output:
=== Student Score Tracker ===
Student 1: Score 72 (new entry)
Grade: C | Rank: #1 | Students below: 0
Leaderboard: [72]
Student 2: Score 88 (new entry)
Grade: B | Rank: #1 | Students below: 1
Leaderboard: [72, 88]
Student 3: Score 55 (new entry)
Grade: E | Rank: #3 | Students below: 0
Leaderboard: [55, 72, 88]
Student 4: Score 91 (new entry)
Grade: A | Rank: #1 | Students below: 3
Leaderboard: [55, 72, 88, 91]
Student 5: Score 72 (duplicate — tie detected)
Grade: C | Rank: #3 | Students below: 1
Leaderboard: [55, 72, 72, 88, 91]
Student 6: Score 63 (new entry)
Grade: D | Rank: #5 | Students below: 1
Leaderboard: [55, 63, 72, 72, 88, 91]
Student 7: Score 47 (new entry)
Grade: F | Rank: #7 | Students below: 0
Leaderboard: [47, 55, 63, 72, 72, 88, 91]
Student 8: Score 88 (duplicate — tie detected)
Grade: B | Rank: #2 | Students below: 5
Leaderboard: [47, 55, 63, 72, 72, 88, 88, 91]
Student 9: Score 76 (new entry)
Grade: C | Rank: #4 | Students below: 5
Leaderboard: [47, 55, 63, 72, 72, 76, 88, 88, 91]
Student 10: Score 95 (new entry)
Grade: A | Rank: #1 | Students below: 9
Leaderboard: [47, 55, 63, 72, 72, 76, 88, 88, 91, 95]
=== Final Leaderboard ===
[47, 55, 63, 72, 72, 76, 88, 88, 91, 95]
=== Grade Distribution ===
Score 47: F
Score 55: E
Score 63: D
Score 72: C
Score 72: C
Score 76: C
Score 88: B
Score 88: B
Score 91: A
Score 95: A