Two behaviours, one helpful and one not, and the difference between them is whether the number went below zero.
lines = ['a', 'b', 'c', 'd', 'e'] lines[1:3] # -> ['b', 'c'] the end is exclusive lines[1:99] # -> ['b','c','d','e'] a big end clamps, harmlessly lines[-2:4] # -> ['d'] a negative START counts from the end # so index 0 with radius 2 becomes lines[-2:3], not lines[0:3] max(0, -2) # -> 0 clamping, on purpose
- The end is exclusive, so the window needs a + 1
lines[index - radius : index + radius] stops just before the last line you wanted, so the snippet is symmetric in intent and lopsided in practice: radius lines before, radius minus one after. It is the commonest arithmetic slip in Python, and the clue is always that one end of the result is short.
- A negative start is not a small start
Python reads a negative index as counting from the end, so a window near the top of the file quietly becomes a window near the bottom. Nothing raises and the output is a perfectly plausible list of lines — from the wrong place. This is why max(0, ...) is not defensive noise here: it is the only thing standing between you and text from another part of the file.
- Only the start needs clamping
An end past the length is already handled — lines[1:99] gives what there is and raises nothing. So the fix is asymmetric: max() on the start, nothing on the end. Writing min(len(lines), ...) as well is harmless and suggests the author had not worked out which half was the problem.
There are two bugs and they are at opposite ends of the same expression. Work out what the slice actually asks for when index is 0 and radius is 2, then count the lines the middle case returns.
A slice end is exclusive, so reaching radius lines past the match needs one more than you think. And a negative start does not mean 'the beginning' — it means counting backwards from the end.
start = max(0, index - radius) and end = index + radius + 1. The end needs no clamping, because a slice already stops at the length; adding a min() there would be harmless and would suggest you had not worked out which end was the problem.
def context_lines(lines, index, radius): if index < 0 or index >= len(lines): raise ValueError(f’index {index} is outside the file') if radius < 0: raise ValueError(f’radius cannot be negative, got {radius}') start = ____(0, index - radius) end = index + radius + ____ return lines[start:____]
def context_lines(lines, index, radius):
if index < 0 or index >= len(lines):
raise ValueError(f"index {index} is outside the file")
if radius < 0:
raise ValueError(f"radius cannot be negative, got {radius}")
# max(), because a negative start would count from the end of the
# file rather than clamping at the beginning.
start = max(0, index - radius)
# + 1, because a slice end is exclusive.
end = index + radius + 1
return lines[start:end]
The missing + 1 was visible on every input; the missing max() only appeared within radius lines of the top. That is why the first ticket arrived immediately and the second took a week — and why fixing the obvious one and shipping would have left the worse bug in place. When a report says two things, check whether it is two bugs.
lines[max(0, index - radius):index + radius + 1] is one line and correct and nearly unreadable. Two names turn it into three statements you can check independently — and give the comments somewhere to attach, which for arithmetic this fiddly is most of the value.
Near either edge you get what exists, which is what callers want — and it means the length is not something they can assume. If the caller needs to know where the window started, return that too: a snippet without its line numbers is much less useful, and the caller cannot recompute it once the clamping has happened.
For large files, do not build the list of lines at all — itertools.islice over the file object gives the same window without reading the whole thing into memory, and collections.deque(maxlen=radius) keeps the preceding lines while you stream. If you are reimplementing grep -C over many files, ripgrep does it in a fraction of the time and has already thought about binary files and encodings. And if the caller needs line numbers, return the start index alongside the lines — this function’s clamping is exactly the information they cannot get back afterwards.
- Return the starting line number alongside the lines, and check it against every edge case here.
- Take separate before and after radii, and work out which of the two fixes each one needs.
- Write the streaming version with a deque, and confirm it agrees with this one on every case.