LESSON 55 · DEEPER PYTHON27 min

Find Insertion Points with bisect

Use bisect_left on a sorted sequence to locate an insertion point even when the value is absent.

Before you start: Lists · Loops · Functions

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Find Insertion Points with bisect

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Suppose a leaderboard stores scores in ascending order: ten, twenty, twenty, and forty. A new score of twenty-five is not present, but it has a clear place between the last twenty and forty. We need its insertion position, not an existing element. Python offers bisect_left for this exact question. It searches a sorted sequence and returns the first position where the target can be inserted without breaking order. We will first run a familiar list.index approach and see why it fails when the target is absent. Then we will probe positions at both ends and around duplicate values, build a function that returns a new list instead of mutating the caller's data, and verify the edge cases.

  2. 00:44

    The companion lesson includes a browser console, worked examples, a checked task, common mistakes, and a quiz. Before we touch code, point to the place twenty-five should occupy. Is that an error because the value is missing, or is it the answer we actually want? Also ask which side of equal scores a new twenty should go. Both sides preserve sorted order, so we must choose a policy. This lesson chooses the left side. list.index asks where a matching element already exists. There is no twenty-five, so it raises ValueError. The scores themselves remain unchanged. Catching the exception keeps our lesson running, but it does not answer the ranking question.

  3. 01:27

    A common workaround is to append the value, sort everything again, and search for it. That can be perfectly reasonable for one tiny list, but it does extra work and may mutate the original list if done carelessly. We already know scores are sorted. What we want is the boundary between values smaller than twenty-five and values greater than or equal to it. Binary search can find that boundary without scanning every element. The important precondition is sorted input. If scores were twenty, ten, forty, the boundary reasoning would fail; bisect does not silently sort for us. This is a useful debugging distinction: a failed index call does not mean the target is invalid.

  4. 02:11

    It means we asked for existence when we needed placement. Let us run several searches with bisect_left and read the returned indexes as positions between elements. One of those valid positions will equal the length of the list. Five belongs at index zero, before ten. Twenty returns index one, before the two existing twenties. Twenty-five returns index three, between the second twenty and forty. Fifty returns index four, which equals the list length and means append at the end. That is not an invalid index for insertion; it would only be invalid if we tried to read scores at index four before inserting. bisect_left chooses the left edge of a run of equal values. bisect_right would return the right edge instead.

  5. 02:59

    Neither changes the input. We can use the difference between right and left to count equal values, but our task needs only the left position. This search uses logarithmic comparisons in a sorted sequence, which is why the module is called bisect. Inserting into a Python list afterward can still shift many items, so the entire operation is not guaranteed logarithmic. Keep search and storage costs separate. For a small leaderboard the list approach is clear. For millions of frequently inserted records, a different data structure might make sense. Now we will create a new list, insert the score at the computed position, and preserve the original for the caller.

  6. 03:43

    The returned index is one. The new list has three twenties, with the incoming one inserted at the left edge of the equal run. We cannot see which identical twenty is new just by printing the values, but the reported index makes the chosen rule observable. The original scores still contain four elements because we inserted into result, a copy. This function assumes an ascending list of comparable integer scores. It does not validate or sort input; that is part of its documented contract. If you need to accept unsorted scores, decide whether to sort a copy first and what that means for the rank you report.

  7. 04:23

    Silently sorting can change the interpretation of existing positions. Notice that list.insert takes a position and a value, while bisect_left only returns the position. Those are two separate operations. You could also use insort_left, which combines them, but it mutates the supplied list. Here our explicit copy supports the promise that the caller's data remains unchanged. The browser exercise asks you to implement this exact pair of returned values. Start with the bisect result, create a copy, insert there, and test a duplicate before testing a new middle value. These assertions cover the duplicate policy, both ends of a populated list, empty input, and the untouched caller list.

  8. 05:09

    They prove more than a single happy-path example. On an empty list, bisect_left returns zero and insert creates a one-element result. On a score larger than all existing values, the position equals the original length. At the left boundary, the new score becomes the first element. If a browser check fails, print both the returned index and the new list. A correct list paired with the wrong index still violates the task contract. Also check that you did not call insert on scores itself, because the task promises a new list. The key idea is not memorizing one module function name. It is asking the right question of sorted data: do I want to find an existing value, or find a place where a value belongs? list.index answers the first; bisect_left answers the second.

  9. 06:00

    State the sorted-input precondition, choose a side for duplicates, and separate the fast search from the potentially linear list insertion. Once those choices are explicit, the implementation becomes small and predictable.

Understand the concept

list.index(value) searches for an existing equal value and raises ValueError when it is absent. In many sorted-data tasks, we want a position even when the value does not exist yet. bisect_left returns the first index where a value can be inserted without breaking sorted order. The returned position ranges from zero to len(sequence), so it works at both ends as well as in the middle.

The algorithm relies on the input already being sorted under the same ordering. It does not sort for you, and calling bisect on an unsorted list can return a position that looks plausible but is wrong. bisect_left searches in logarithmic comparisons. Inserting into a Python list still shifts elements and may cost linear time; do not describe the whole insert operation as logarithmic.

The task rank_and_insert(scores, score) returns the insertion index and a new sorted list containing score. Duplicate scores go before existing equal values because we use bisect_left rather than bisect_right. The original list must remain unchanged. The exercise assumes integer scores in ascending order; it does not silently resort malformed input.

For repeated membership checks, a set may be more appropriate, but sets discard sorted position. For repeated inserts into a very large collection, a list may not be the right storage structure. bisect is valuable when sorted order, a compact list, and an insertion position are exactly what the application needs.

  • bisect_left returns the leftmost insertion point
  • Input must already be sorted
  • Searching is logarithmic; list insertion can be linear
  • A copy preserves the caller's list

See it step by step

Read the code, predict the output, then compare it with the result.

01. Find positions for present and absent values

PYTHON
from bisect import bisect_left
scores = [10, 20, 20, 40]
for value in [5, 20, 25, 50]:
    print(value, bisect_left(scores, value))
EXPECTED OUTPUT
5 0
20 1
25 3
50 4

A missing value still has an insertion point, including before the first or after the last item.

02. Compare left and right positions for ties

PYTHON
from bisect import bisect_left, bisect_right
scores = [10, 20, 20, 40]
print(bisect_left(scores, 20))
print(bisect_right(scores, 20))
print(scores)
EXPECTED OUTPUT
1
3
[10, 20, 20, 40]

left points before equal values, right points after them, and neither search mutates the list.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Find a position even when the value is absent

list.index asks where an equal item already exists. A ranking list often needs a different question: where should a new score go? bisect_left answers that for an ascending sorted list, returning an index from zero to the list length. It does not require the target score to be present.

Read the four results as four insertion locations. Five belongs before the first score, twenty belongs before its equal values, twenty-five belongs between the twenties and forty, and fifty belongs after everything. A position of len(scores) is valid; it is not an out-of-range error until you try to read an element there.

PYTHON
from bisect import bisect_left
scores = [10, 20, 20, 40]
for target in [5, 20, 25, 50]:
    print(target, bisect_left(scores, target))
EXPECTED OUTPUT
5 0
20 1
25 3
50 4
Follow the reasoning
  1. The search assumes scores are already sorted.
  2. An absent target still has a place.
  3. The returned index may equal the sequence length.
02 / 03

Choose a tie policy deliberately

Equal values create more than one valid sorted insertion position. bisect_left chooses the first position before equal values, while bisect_right chooses the position after them. Both preserve sorted order, but an application may care whether a new equal score appears ahead of or behind existing entries.

The two functions only search; neither changes scores. This makes them useful for counting how many values equal a target: right minus left gives the size of the equal-value range in a sorted sequence. For this exercise we choose the left position, then use a separate list.insert call on a copy.

PYTHON
from bisect import bisect_left, bisect_right
scores = [10, 20, 20, 40]
left = bisect_left(scores, 20)
right = bisect_right(scores, 20)
print(left, right)
print(right - left)
print(scores)
EXPECTED OUTPUT
1 3
2
[10, 20, 20, 40]
Follow the reasoning
  1. left points before the two equal twenties.
  2. right points after them.
  3. Neither search mutates the list.
03 / 03

Separate search cost from insertion cost

The final function finds the leftmost location, copies the caller's list, and inserts into that copy. The original scores stay available for later comparisons. A boundary case with an empty list returns index zero and a single-element result; a value larger than every current score returns the end position.

Binary search uses logarithmic comparisons, but list.insert may shift many values to make room. That means the entire operation can still take linear time. The input must already be sorted in ascending order. If it is not, first decide whether the caller should sort it, reject it, or use a different data structure; silently applying bisect to unsorted data is unsafe.

PYTHON
from bisect import bisect_left

def rank_and_insert(scores, score):
    index = bisect_left(scores, score)
    result = list(scores)
    result.insert(index, score)
    return index, result

scores = [10, 20, 20, 40]
print(rank_and_insert(scores, 20))
print(scores)
EXPECTED OUTPUT
(1, [10, 20, 20, 20, 40])
[10, 20, 20, 40]
Follow the reasoning
  1. bisect_left finds the leftmost valid index.
  2. list(scores) makes an independent container.
  3. insert changes only that new list.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnImplement rank_and_insert(scores, score) for ascending integer scores. Return the leftmost insertion index and a new list containing score in sorted order. Do not change scores. The prepared result is (1, [10, 20, 20, 20, 40]).
Need a hint?

Compute index = bisect_left(scores, score), then copy scores with list(scores) and call insert(index, score) on the copy.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does bisect_left return for a value missing from a sorted list?

    02.Which precondition matters for bisect_left?

    03.Why is inserting into a list not necessarily logarithmic?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Using index for an absent item

    COMMON MISTAKE
    position = scores.index(25)
    FIX
    position = bisect_left(scores, 25)

    What happens: ValueError is raised because 25 is not present.
    bisect_left finds an insertion position whether or not the value exists.

    Searching unsorted values

    COMMON MISTAKE
    scores = [20, 10, 40]
    position = bisect_left(scores, 15)
    FIX
    scores = sorted(scores)
    position = bisect_left(scores, 15)

    What happens: The returned position cannot guarantee a sorted result.
    Establish sorted order before binary search; decide separately whether sorting a copy fits your contract.

    Browse this lesson’s problem fixes

    Sources and further reading