The skeleton of every 'longest / largest / best' loop. Two variables, and two decisions people get wrong.
# Wrong in two ways at once
best, current = [], []
for item in items:
if current and item == current[-1]:
current.append(item)
else:
if len(current) >= len(best): # >= hands ties to the later run
best = current
current = [item]
return best # never sees the final run
# The fix for both: compare on every step, and use >
if len(current) > len(best):
best = list(current)
- Comparing inside the loop removes the flush entirely
Checking 'is this the best so far' after every single item, rather than only when a run ends, means the final run has already been considered by the time the loop stops. The version that only compares at a boundary has to repeat the comparison after the loop — and forgetting that line is the single most common bug in this shape of code.
- > keeps the first, >= keeps the last
The comparison operator is where the tie-breaking rule lives, and it is one character wide. With >, a later run of equal length never displaces the earlier one; with >=, the last one always wins. Neither is more correct in general — but one of them is what your contract says, and it is worth a comment or a test rather than being left to chance.
- current[-1] is the whole comparison
A run continues when the new item equals the previous one, and the previous one is always the last thing added to the current run. No index arithmetic, no lookahead, no need to handle 'the first item' as a special case — `if current and ...` covers it, because an empty run cannot be continued.
Two variables: the run you are currently building, and the best one you have seen. Every item either continues the current run or starts a new one — and either way, you can ask whether it is now the best.
An item continues the run when the run is not empty and the item equals its last element. Otherwise start a fresh run holding just that item. Then, on every iteration, compare the current run’s length against the best one’s.
Comparing on every iteration rather than only when a run ends is what removes the need for a check after the loop. Use > and not >= so the earliest run wins a tie, and store a copy — list(current) — so that later appends to the current run do not quietly grow the one you saved.
def longest_run(items): best = [] current = [] for item in items: if current and item == current[____]: current.append(item) else: current = [____] if len(current) ____ len(best): best = ____(current) return best
def longest_run(items):
best = []
current = []
for item in items:
if current and item == current[-1]:
current.append(item)
else:
current = [item]
# Checking here, rather than when a run ends, is what makes the
# final run count without a second comparison after the loop.
if len(current) > len(best):
best = list(current)
return best
Without it, best and current are the same object — so the next append to the current run silently extends the saved best one too. The answer would still be right here, by luck, because a growing run is also the new best. Rely on that and the function breaks the moment anyone adds a condition to the comparison. Aliasing bugs of this kind are invisible until they are expensive.
(value, length) is more compact and throws away which items they were — which matters as soon as the items carry anything beyond their identity, such as timestamped events that happen to share a status. Returning the slice lets the caller ask len() themselves, or look at the first element, or take the last. Give back the richer thing when it costs nothing.
Sorting first would find the most common value, not the longest run, and the two are different questions with different answers — a value can appear thirty times without ever appearing twice in a row. If you catch yourself reaching for sorted() or Counter here, check which question you are actually answering.
Every version below is correct and verified against the same cases by the build. They are here because knowing which one to reach for is the part that takes judgement, and the trade-off is stated rather than left for you to guess.
from itertools import groupby
def longest_run(items):
return max((list(group) for _, group in groupby(items)),
key=len, default=[])
The idiomatic version. groupby yields one group per stretch of consecutive equal items, which is exactly the loop above, and max with key=len keeps the first of any tie — so the contract’s tie rule comes free. The default=[] is doing real work: max on an empty sequence raises ValueError, and an empty input is a legitimate call here.
itertools.groupby already does the grouping half of this in one line, and it does it lazily, which matters when the input is a stream rather than a list. If you need every run and not just the longest, groupby is the answer outright. For very large numeric arrays, the vectorised trick — find where the value changes, then take the differences between those positions — is dramatically faster in numpy than any Python loop. Write the loop when the items are objects, when the comparison is more than equality, or when you want to stop early.
- Return every run of the maximum length instead of just the first, and decide what the empty case should give back now.
- Take a key function, so runs of records sharing a status count as consecutive equal items — the same change groupby’s key argument makes.
- Return the start position alongside the run, and check it against a case where the same value forms two separate runs.