Run it
Both boundaries, and the three questions one pair of calls answers.
By hand: the two loops differ by a single character.
Absent values, and the key= parameter that does not transform the needle.
Why a plain search is not enough
With duplicates, the standard loop stops at whichever occurrence mid hit first. That is a correct answer to "is it there" and no answer at all to "where does the run start" — and scanning left from the hit is O(n) in the worst case, which throws away the reason for using binary search.
The fix is to stop searching for the value and start searching for the boundary. On finding a match, do not return: keep going in the direction of the edge you want. That is the only change, and it is what both bisect functions do.
left and right, precisely
bisect_left(a, x) returns the first index i such that a[i] >= x. So everything before it is strictly less than x, and if x is present, i is its first occurrence.
bisect_right(a, x) returns the first index i such that a[i] > x. So it is one past the last occurrence, and the last index is right - 1.
Three consequences, and they are why the pair is worth memorising. left == right means absent. right - left is the count. And a[left:right] is the run itself, as a slice.
Writing it by hand
The interviewer usually wants the loop, not the import. It is the half-open search from the previous question with one comparison changed:
def lower(a, x): # bisect_left
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < x: lo = mid + 1
else: hi = mid
return lodef upper(a, x): # bisect_right lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] <= x: lo = mid + 1 # <= is the only difference else: hi = mid return lo
One character. < stops at the first element not less than x; <= steps over every element equal to it. Being able to point at that character and say what it does is the whole question.
The key= trap
bisect gained a key= parameter in Python 3.10, and it behaves differently from sorted(key=...) in a way that catches people: the needle is not passed through the key. So searching a list of records by one field means comparing against the field value directly, not against a record:
i = bisect_left(people, 30, key=lambda p: p.age)
Before 3.10 the standard workaround was to keep a parallel list of just the keys and search that — which is still what you do when the key is expensive to compute, because key= calls it on every probe.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
Absent values. left == right, and the shared index is still useful - it is where the value would go.
A value larger than everything. bisect_left returns len(a), so indexing a[lo] raises. Check the bound before the equality.
The whole array is the value. left = 0 and right = len(a), so the last index is len(a) - 1 - worth checking, because it is where a missing minus-one shows up.