Slicing
Overview
Slicing extracts a sub-sequence with `sequence[start:stop:step]`. Any of the three can be omitted: `start` defaults to 0, `stop` defaults to the sequence's length, `step` defaults to 1. `stop` is exclusive, so a slice covers a half-open interval — `items[2:5]` returns indices 2, 3, 4, never 5. That's why `len(items[a:b]) == b - a` (clamped to the sequence bounds), a handy sanity check. Negative indices count from the end (`items[-1]` is the last element), and negative indices work inside slices too: `items[-3:]` means 'last 3 elements'. A negative `step` reverses direction: `items[::-1]` reverses the whole sequence — it walks from the end to the start. The critical property for backend code: slicing ALWAYS returns a new list (or string, or tuple) — it never returns a view into the original, and it never raises `IndexError` even if the bounds are out of range. `items[100:200]` on a 3-element list just returns `[]`, silently. That's convenient for pagination logic (no bounds-checking needed) but dangerous if you actually expected an error to catch a bad offset. page = items[offset:offset + limit] # never crashes, even if offset is way past the end page is a NEW list — mutating page does not touch items
Slicing is the standard tool for in-memory pagination and batching before it's worth pushing that logic into a DB query, and its never-raises, always-copies behavior is exactly what makes it safe to reach for without extra bounds-checking or defensive-copy code.
Where used: In-memory pagination: `results[offset:offset + page_size]`, String truncation for logs: `message[:200] + '...' if len(message) > 200 else message`, Reversing a sequence: `history[::-1]` to show most-recent-first
Why learn this
- Slicing is the standard tool for in-memory pagination and batching before it's worth pushing that logic into a DB query — knowing it never raises IndexError means you can skip defensive bounds-checks.
- Because a slice always returns a new object, it's a common (and correct) way to make a defensive copy of a list you're about to hand to code that might mutate it.
Code walkthrough
items = [1, 2, 3, 4, 5, 6, 7]
page = items[2:2+3]
print(page)
page.append(999)
print("page:", page)
print("items:", items)
Focus: Watch that mutating `page` after the slice has zero effect on `items` — the slice created a fully independent list, it did not hand back a window into the original.
Common mistakes
- Assuming an out-of-range slice raises IndexError: Direct indexing (`items[100]`) raises `IndexError` on a 3-element list, but slicing (`items[100:200]`) just returns `[]`. If your pagination code relies on an exception to detect 'offset beyond the data', it will never fire — you have to check length explicitly instead.
- Confusing `items[:n]` (a copy) with `items` (a reference): `items[:]` (a full slice) makes a real, independent copy — the classic Python idiom for shallow-copying a list. Writing just `items` instead assigns a second name to the SAME list, so mutating one mutates both. This distinction matters anywhere you pass a list into a function that might append/sort/clear it.
Glossary
- slice
- A `start:stop:step` expression inside `[]` that extracts a sub-sequence from a list, tuple, or string, returning a NEW object rather than a reference into the original. Example: `items[2:5]`
- half-open interval
- A range that includes its start but excludes its end — `[start, stop)`. Python slices work this way: `items[2:5]` includes index 2, 3, 4 but not 5, so `stop - start` always equals the slice's length. Example: `items[0:0]` is empty
Recall questions
- What does `items[2:5]` include, and why is `len(items[2:5])` equal to 3?
- Does `results[offset:offset + limit]` raise an error if `offset` is larger than `len(results)`?
- Is `page = items[offset:offset+limit]` a new list, or a view into `items`? Why does that matter for a paginated API response?
Understanding checks
What does this print?
[30, 40, 50] [10, 20, 30] [50, 40, 30, 20, 10]
`items[-3:]` means 'from the 3rd-from-last element to the end' -> last 3 elements. `items[:-2]` means 'from the start up to (not including) the 2nd-from-last' -> drops the last 2. `items[::-1]` uses step -1, which walks the whole sequence backward, reversing it.
A function does `page = data[offset:offset+limit]` where `offset=1000` and `data` has 5 elements. It returns `[]` with no error. Is this a bug, and how would you detect 'offset is beyond the data' if the caller needs to know?
Not a bug in the slicing itself — this is correct, documented slice behavior. If the caller needs to distinguish 'valid empty result' from 'offset out of range', that check has to be explicit: compare `offset` against `len(data)` before or after slicing, since the slice operation itself will never signal it.
Slicing's silent clamping is a feature for iteration convenience, but it means bounds validation is the caller's job whenever an out-of-range offset is meaningfully different from a legitimately empty page.
Practice tasks
Continue learning
Previous: Slicing
Next: Comprehensions