When the boundaries are sorted, you don't need to walk through them. bisect finds where a value would slot in by halving the list each step, so 1,000,000 boundaries take about 20 steps. It also reads better than a ladder of if statements.
Before
def grade(score): if score >= 90: return "A" elif score >= 80: return "B" elif score >= 70: return "C" elif score >= 60: return "D" return "F" print([grade(s) for s in [95, 83, 70, 61, 42]])
['A', 'B', 'C', 'D', 'F']
Fine for five grades. Change a boundary or add a band and you are editing code, not data.
After
The boundaries become data. bisect_right returns how many boundaries the score has reached, and that number picks the grade.
from bisect import bisect_right BOUNDARIES = [60, 70, 80, 90] GRADES = "FDCBA" def grade(score): return GRADES[bisect_right(BOUNDARIES, score)] print([grade(s) for s in [95, 83, 70, 61, 42]])
['A', 'B', 'C', 'D', 'F']
bisect_right puts a score that sits exactly on a boundary (70) above it, into band C. bisect_left would put it below. Pick the one that matches your rule.
Keeping a list sorted as it grows
insort puts a new value in the right place, so the list never needs sorting again:
from bisect import insort, bisect_left prices = [4.99, 9.99, 14.99, 24.99] insort(prices, 12.49) print(prices) budget = 13.00 affordable = prices[:bisect_left(prices, budget)] print(affordable)
[4.99, 9.99, 12.49, 14.99, 24.99] [4.99, 9.99, 12.49]
Why it works
Each comparison rules out half of what is left, so the number of steps grows with the logarithm of the length, not the length. A scan through a million items might check them all; bisect checks about 20.
When not to use it
The list must already be sorted: on unsorted data bisect quietly returns nonsense rather than failing. insort still has to shift items along to make room, so for thousands of inserts into a huge list a different structure fits better. And if you're only asking "is this value present?", a set is simpler.