Fix tail(n, seq) returning whole sequence for n <= 0 - #627
Conversation
tail(n, seq) is implemented as seq[-n:] with a deque(seq, n) fallback for non-sliceable inputs. For n == 0, seq[-0:] is seq[0:] -- the entire sequence -- so tail(0, seq) returned everything instead of the last zero elements. Worse, the two input paths disagreed: sliceable inputs returned the whole sequence while non-sliceable iterables hit deque(seq, 0) and correctly returned empty. Negative n was inconsistent too (seq[1:] for a list vs. ValueError: maxlen must be non-negative for an iterator). Guard n <= 0 up front and return an empty tuple, matching both the sibling take(0, seq) (which already returns empty) and the value the deque fallback already produced. The n >= 1 slice path -- and its type-preserving behaviour (str/tuple/list in, same type out) -- is unchanged. Add regression coverage in test_tail asserting empty and consistent results across sliceable and non-sliceable inputs for n == 0 and n < 0. Fixes pytoolz#626
|
@Sanjays2402, tell your AI agent that the code is inefficient and wrong. wrongIf the user passes a negative number for inefficientHow often is Z0Z_tools try:
return seq[len(seq) - n: None]
except (TypeError, KeyError):
return tuple(deque(seq, n))If an object doesn't have |
Per maintainer feedback on the PR: - A negative n now raises ValueError instead of silently returning empty, matching nth's contract and surfacing the caller's upstream bug rather than masking it. - Replace the `if n <= 0: return ()` special-case with a single slice `seq[max(len(seq) - n, 0):]`. This handles n == 0 (empty tail) inline without a branch that runs on every call, while the max(..., 0) clamp keeps n > len(seq) returning the whole sequence (a plain seq[len(seq) - n:] would wrap to a negative index and truncate). Tests updated to assert the raise-on-negative contract and add explicit n > len coverage for both sliceable and iterator inputs.
|
Both points taken, and
One thing on the slice: return seq[max(len(seq) - n, 0):]The Tests cover |
Fixes #626
The bug
tail(n, seq)is implemented asseq[-n:]with adeque(seq, n)fallback for non-sliceable inputs. Forn == 0,seq[-0:]isseq[0:]— the entire sequence — sotail(0, seq)returned everything instead of the last zero elements.The two input paths also disagreed with each other:
Negative
nwas inconsistent too —tail(-1, [10, 20, 30])returned[20, 30](seq[1:]), whiletail(-1, iter([10, 20, 30]))raisedValueError: maxlen must be non-negative.By contrast the sibling
take(0, seq)already correctly returns empty.Root cause
One line:
seq[-n:]withn == 0slicesseq[0:](the whole sequence, since-0 == 0). Negativenslices from the front and, on the fallback path, passes a negativemaxlentodeque.The fix
toolz/itertoolz.py— guardn <= 0up front and return an empty tuple, matching bothtake(0, seq)and the value thedequefallback already produced:The
n >= 1slice path is untouched, so type-preserving behaviour is unchanged (strin →strout,tuple→tuple,list→list).last(seq)— which callstail(1, seq)[0]— is unaffected because it always passesn == 1.Before / after
tail(0, [10, 20, 30])[10, 20, 30]()tuple(tail(0, iter([10, 20, 30])))()()tail(-1, [10, 20, 30])[20, 30]()tuple(tail(-1, iter([10, 20, 30])))ValueError()tail(2, 'ABCDE')'DE''DE'(unchanged)tail(2, (3, 2, 1))(2, 1)(2, 1)(unchanged)Test
Extended
test_tailwith regression assertions coveringn == 0andn < 0across both sliceable and non-sliceable inputs. Proven to guard the bug:test_tailFAILS:assert ['A', 'B', 'C', 'D', 'E'] == []test_tailPASSESFull suite green, no regressions:
pycodestyle --ignore="E731,W503,W504,E402" toolz/itertoolz.pyis clean.Note: the same latent bug exists in
cytoolz; happy to open a companion PR there if you'd like to keep the two implementations in parity.