From a1846082cc92d54d920dcfa4d0a7475018143667 Mon Sep 17 00:00:00 2001 From: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:53:33 -0700 Subject: [PATCH 1/2] Fix tail(n, seq) returning whole sequence for n <= 0 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 #626 --- toolz/itertoolz.py | 2 ++ toolz/tests/test_itertoolz.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/toolz/itertoolz.py b/toolz/itertoolz.py index 354ecf25..44fd8bf5 100644 --- a/toolz/itertoolz.py +++ b/toolz/itertoolz.py @@ -332,6 +332,8 @@ def tail(n, seq): drop take """ + if n <= 0: + return () try: return seq[-n:] except (TypeError, KeyError): diff --git a/toolz/tests/test_itertoolz.py b/toolz/tests/test_itertoolz.py index c8640917..a91cede7 100644 --- a/toolz/tests/test_itertoolz.py +++ b/toolz/tests/test_itertoolz.py @@ -189,6 +189,15 @@ def test_tail(): assert list(tail(3, 'ABCDE')) == list('CDE') assert list(tail(3, iter('ABCDE'))) == list('CDE') assert list(tail(2, (3, 2, 1))) == list((2, 1)) + # tail(n, seq) for n <= 0 is the last zero (or fewer) elements: empty. + # Regression for gh #626: seq[-0:] == seq[0:] wrongly returned the whole + # sequence for sliceable inputs, while the deque fallback returned empty, + # so the two input paths disagreed. Both must now be empty and consistent. + assert list(tail(0, 'ABCDE')) == [] + assert list(tail(0, iter('ABCDE'))) == [] + assert list(tail(0, (3, 2, 1))) == [] + assert list(tail(-2, 'ABCDE')) == [] + assert list(tail(-2, iter('ABCDE'))) == [] def test_drop(): From ae58ef2ec789ffbc887d4647b598b835e1655602 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:49:01 -0700 Subject: [PATCH 2/2] Address review: raise on negative n, drop per-call zero special-case 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. --- toolz/itertoolz.py | 12 +++++++++--- toolz/tests/test_itertoolz.py | 17 +++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/toolz/itertoolz.py b/toolz/itertoolz.py index 44fd8bf5..e847de0e 100644 --- a/toolz/itertoolz.py +++ b/toolz/itertoolz.py @@ -328,14 +328,20 @@ def tail(n, seq): >>> tail(2, [10, 20, 30, 40, 50]) [40, 50] + ``n`` must be non-negative; a negative ``n`` raises ``ValueError`` rather + than silently masking an upstream bug in the caller. + See Also: drop take """ - if n <= 0: - return () + if n < 0: + raise ValueError("n must be non-negative") try: - return seq[-n:] + # seq[len(seq) - n:] is correct for 0 <= n <= len(seq); clamp the + # lower bound so n > len(seq) returns the whole sequence instead of + # wrapping to a negative index, and n == 0 returns the empty tail. + return seq[max(len(seq) - n, 0):] except (TypeError, KeyError): return tuple(collections.deque(seq, n)) diff --git a/toolz/tests/test_itertoolz.py b/toolz/tests/test_itertoolz.py index a91cede7..480ea50c 100644 --- a/toolz/tests/test_itertoolz.py +++ b/toolz/tests/test_itertoolz.py @@ -189,15 +189,20 @@ def test_tail(): assert list(tail(3, 'ABCDE')) == list('CDE') assert list(tail(3, iter('ABCDE'))) == list('CDE') assert list(tail(2, (3, 2, 1))) == list((2, 1)) - # tail(n, seq) for n <= 0 is the last zero (or fewer) elements: empty. - # Regression for gh #626: seq[-0:] == seq[0:] wrongly returned the whole - # sequence for sliceable inputs, while the deque fallback returned empty, - # so the two input paths disagreed. Both must now be empty and consistent. + # n larger than the sequence returns the whole sequence. + assert list(tail(10, 'ABC')) == list('ABC') + assert list(tail(10, iter('ABC'))) == list('ABC') + # tail(0, seq) is the last zero elements: empty, consistently for any + # iterable. Regression for gh #626: seq[-0:] == seq[0:] wrongly returned + # the whole sequence for sliceable inputs, while the deque fallback + # returned empty, so the two input paths disagreed. assert list(tail(0, 'ABCDE')) == [] assert list(tail(0, iter('ABCDE'))) == [] assert list(tail(0, (3, 2, 1))) == [] - assert list(tail(-2, 'ABCDE')) == [] - assert list(tail(-2, iter('ABCDE'))) == [] + # A negative n is a programming error (a masked upstream bug), so it + # raises rather than silently returning empty, matching nth's contract. + assert raises(ValueError, lambda: tail(-2, 'ABCDE')) + assert raises(ValueError, lambda: tail(-2, iter('ABCDE'))) def test_drop():