Slice Python Sequences Without Losing the Edges
Predict and use sequence slices, including omitted bounds, negative indices, and a negative step.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Slice Python Sequences Without Losing the Edges
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Suppose a dashboard lets a reader request the last N events. If N is two, we expect the final two events. If N is zero, we expect an empty result. Python's familiar tail slice, items[-N:], handles the first case, but it fails the second in a surprising way: minus zero is zero. A request for zero events returns every event. This is a good example of why a compact expression still needs boundary tests. In this lesson we will first reproduce the bug, then map slice start, stop, and step onto actual list indexes. We will walk a list backward with a negative step and explain what is included.
- 00:41
Finally, we will repair a small last_items function and test empty input, oversized counts, negative counts, and preservation of the source list. The browser lesson has a checked task, so you can repeat each experiment rather than only watch the output. The first output looks perfect. Negative two means start two positions from the end and continue to the end. The second output violates the specification. Python evaluates -0 as 0, and events[0:] is the whole list. This is not a Python slicing defect; it is a mismatch between a mathematical count and an index expression. The code also says nothing about negative counts.
- 01:23
A negative count such as minus one becomes a positive slice start of one and would return everything except the first item. That has no sensible meaning if count is supposed to describe how many final events to return. We should define the function's contract before patching: nonnegative counts only, zero gives an empty new list, a count beyond the list length gives all available elements, and the input stays untouched. With those rules, the repair will be straightforward and testable. The first slice starts at index one and stops before index four. The stop is excluded. The second slice begins at index four, moves backward by two, visits index two, and stops before index one.
- 02:09
A negative step changes direction, but it does not change the rule that the stop is excluded. The third slice omits both bounds and walks backward by one, returning a reverse-order copy. The last line is the source of our bug: negative zero is still zero, so the slice starts at the beginning. Notice that a slice outside the list's range is clipped rather than raising IndexError. That is helpful for a request larger than the list but should not be used to avoid deciding what negative counts mean. Predict letters[-2:] and letters[2:2] now. The first has two elements; the second is empty because its half-open interval has no positions.
- 02:51
The special cases now align with the meaning of a count. A negative request is invalid and gets a clear ValueError. Zero returns a new empty list. Positive counts use the original concise slice. If the requested count exceeds the available items, Python clips the slice to the start and returns all available events in order. We did not mutate events. A list slice creates a new list, so a caller can append to the returned outer list without appending to the original. That is only a shallow separation if the elements themselves are mutable; the next copying lesson will examine that distinction in depth.
- 03:32
Here our events are strings, so the simple slice is sufficient. On the website, replace the starter function with these branches and run the task checker. Watch which cases fail if you remove the zero branch or move the negative check below it. The tests cover the function's branches and its promises. Two returns the expected tail. Zero returns nothing. Nine returns the whole three-element list because that is all the source contains. Empty source remains empty. A negative count raises rather than producing a misleading slice. We also check the original contents and object identity; the result is a new outer list.
- 04:14
These tests would have caught the first one-line version immediately, even though its common count-two example looked fine. Consider a log viewer that asks for its newest zero entries while a filter is active. Returning every entry could expose more data than the interface intended, so the zero boundary matters beyond a toy example. A clear count contract also helps the next developer: zero means none, positive means up to that many, and negative is rejected. If the product instead uses a negative count to mean a reverse view, put that behavior in a separate named option rather than overloading this count. When you apply slicing elsewhere, decide whether you are extracting by indexes or requesting a number of items.
- 05:00
Those are related, but edge cases such as zero can differ. As a follow-up experiment, try a negative step of minus two with different start and stop indexes and write down the visited positions before running it. That small prediction habit makes advanced slices much easier to read and debug.
Understand the concept
A slice reads a portion of a sequence with the form items[start:stop:step]. Start is included; stop is excluded. With a positive step, omitted bounds mean the beginning and end. For a list of five items, items[1:4] selects indexes one, two, and three. Slicing returns a new list and does not modify the source. A slice outside the available range is clipped instead of raising IndexError.
Negative indexes count backward from the end: -1 is the last element. A negative step walks backward. items[::-1] visits every element in reverse order, while items[4:1:-2] visits indexes four and two. When step is negative, the start still belongs to the result and the stop still does not; the direction changes how you choose those bounds.
The compact expression items[-n:] is a useful 'last n' slice for positive n. It has one surprising boundary: when n is zero, -n is also zero, so items[-0:] returns the entire list. A separate zero branch is required if last zero items should mean an empty list. Reject a negative n if your function's contract defines n as a count; otherwise a negative count would produce an unrelated slice.
The practice function returns the last n elements in their original order, without changing the input. It accepts zero and counts larger than the list length. It rejects negative counts with ValueError. The checker uses empty input, zero, a middle count, and an oversized count so that a solution based only on items[-n:] does not pass by accident.
- Slice stop is excluded
- Negative indices count from the end
- A negative step reverses direction
- items[-0:] is the whole list
See it step by step
Read the code, predict the output, then compare it with the result.
01. Read a half-open window
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[1:4])
print(letters[:2])
print(letters[4:9])['b', 'c', 'd']
['a', 'b']
['e']The stop is excluded and an oversized stop is clipped to the end of the list.
02. Walk a list backward
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[::-1])
print(letters[4:1:-2])['e', 'd', 'c', 'b', 'a']
['e', 'c']A negative step changes traversal direction; indexes four and two are visited before exclusive stop index one.
03. Expose the minus-zero edge case
values = [10, 20, 30]
for count in (0, 1, 5):
print(count, values[-count:])0 [10, 20, 30]
1 [30]
5 [10, 20, 30]The zero slice unexpectedly returns the entire list, motivating an explicit count == 0 branch.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Read a half-open slice
The three positions inside brackets are start, stop, and step. The start belongs to the slice, while the stop does not. In the first expression below, indexes one through three are selected and index four is excluded. When start is omitted, the slice begins at the natural beginning for its direction. These rules apply to lists, tuples, strings, and other sliceable sequences.
A slice normally returns a new sequence of the same kind rather than changing its source. A stop beyond the list length is clipped, which differs from directly indexing an out-of-range position. Try to predict the three outputs before running them. Writing the indexes above the list is a reliable way to debug a slice that is one element too short.
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[1:4])
print(letters[:2])
print(letters[4:9])
print(letters)['b', 'c', 'd']
['a', 'b']
['e']
['a', 'b', 'c', 'd', 'e']- Index one is included, but stop index four is not.
- An omitted start uses the beginning.
- An oversized stop is clipped; the source remains unchanged.
Reverse direction with a negative step
A step of minus one visits every position backward. With omitted bounds, letters[::-1] produces a reverse-order copy of the whole list. A step of minus two skips every other position while moving backward. The stop remains excluded even when direction reverses; that is why letters[4:1:-2] visits indexes four and two, but not one.
Negative indexes and negative steps solve different problems. Index minus one names the final element. Step minus one chooses a backward direction. You can use both in one slice, but overly compressed slices become difficult to maintain. For a complex extraction, write down the intended positions first or use an explicit loop when it communicates the rule better.
letters = ['a', 'b', 'c', 'd', 'e']
print(letters[-1])
print(letters[::-1])
print(letters[4:1:-2])e
['e', 'd', 'c', 'b', 'a']
['e', 'c']- Negative one as an index names the last item.
- Negative one as a step walks backward through all items.
- The last slice visits indexes four and two, stopping before one.
Repair a count-based tail
A slice that asks for the last count items seems natural as items[-count:]. It works for positive counts. At zero, however, -count is zero and items[0:] returns everything. The count-based function needs a branch that returns an empty list when count is zero. It should also reject negative counts rather than turning them into an unrelated positive start index.
The corrected function returns a new outer list for every valid count. A count longer than the input is clipped to all available elements, and an empty input stays empty. The task checker tests these boundaries and checks that the input list has not changed. This is the difference between testing a slice expression and testing the actual public contract of a function.
def last_items(items, count):
if count < 0:
raise ValueError('count must be nonnegative')
if count == 0:
return []
return items[-count:]
values = [10, 20, 30]
for count in (0, 2, 9):
print(count, last_items(values, count))0 []
2 [20, 30]
9 [10, 20, 30]- Reject invalid negative counts before slicing.
- Handle zero separately because -0 equals 0.
- Use the concise slice for positive counts and let Python clip large requests.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Reject count < 0 first. If count == 0, return []. Otherwise return items[-count:]. A slice creates the new list.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Forgetting that stop is excluded
items[1:3] # expect indexes 1, 2, 3items[1:4] # indexes 1, 2, 3What happens: The third requested index is missing.
Choose a stop one past the final positive index.
Assuming minus zero means an empty tail
return items[-count:]if count == 0:
return []
return items[-count:]What happens: For count zero, the entire list is returned.
Zero needs its own branch because -0 equals 0.