skip to content

Why does the Python slice items[1:4] return three elements, not four?

level: juniorimportance: must knowfreq 85%

answer

  1. Count the gaps, not the items
  2. One end belongs, the other does not
  3. Length is stop minus start
  4. -1 is the last element
  5. Negative step flips the missing bounds

basics

~20 s

Python slices are half-open: the start index is included and the stop index is excluded. So items[1:4] yields positions 1, 2 and 3, and with the default step the length is simply stop minus start.

solid answer

~40 s

Slice bounds are half-open — `start` is included, `stop` is not — so `items[1:4]` covers positions 1, 2 and 3 and `len(items[1:4]) == 4 - 1`. Two useful properties follow: `s[:k] + s[k:] == s` for every `k`, so consecutive chunks tile a sequence without gaps or duplicates, and an empty slice `s[a:a]` is expressible at all. A negative index is translated to `len(s) + i` before anything else, so `-1` is the last item and `s[-3:]` is the last three. Omitted bounds default to the two ends and `step` defaults to 1; with a negative step those defaults flip direction, which is why `s[::-1]` walks backwards and returns a reversed copy. Slicing a built-in sequence returns a new object of the same type, while indexing returns a single element.

code

python · 5 lines
python
s = "thumbnail"
print(s[1:4], len(s[1:4]))   # hum 3  -> 4 - 1
print(s[-3:])                # ail
print(s[::-1])               # lianbmuht
print(s[:4] + s[4:] == s)    # True

go deeper

for a junior

Recall the rule crisply: start included, stop excluded, so the length is stop minus start. Know that -1 is the last element and that s[::-1] gives a reversed copy. Expect to read a slice aloud from a whiteboard.

for a middle

Explain the mechanics, not just the rule: how a negative bound becomes len(s) plus the bound, how the missing start and stop flip meaning under a negative step, and why s[:k] + s[k:] reconstructs the sequence for any k.

for a senior

Show the cost model. A slice allocates a new sequence of the same type and copies O(k) references, so in a hot path prefer in-place reversal or a lazy iterator, and be ready to say when a slice is the clearer choice anyway.

for a principal

Own the convention as a design argument: half-open intervals make chunking, pagination and range boundaries composable across a codebase, and mixed conventions in team APIs are a recurring source of off-by-one defects worth standardising away.

### Slices name gaps, not items Python's `s[start:stop:step]` is easiest to reason about if you picture the index positions as the *gaps between* elements rather than the elements themselves. For the string `"thumbnail"`, gap 0 sits before `t`, gap 1 between `t` and `h`, and gap 9 after the final `l`. A slice names two gaps and hands back everything between them, so `s[1:4]` is the material between gap 1 and gap 4 — the three characters `hum`. Stated the way an interviewer wants to hear it: the slice is **half-open**, `start` is included and `stop` is excluded. ### Three consequences, and they are the reason for the convention **Length arithmetic is plain subtraction.** With the default step of 1 and both bounds inside the sequence, `len(s[a:b]) == b - a`. No plus-one anywhere. A window of eight items starting at offset `i` is exactly `items[i:i+8]`, and the next window starts at `i+8`. **Slices tile.** `s[:k] + s[k:] == s` holds for every `k`. That identity is what makes chunking loops correct by construction: `s[0:8]`, `s[8:16]`, `s[16:24]` neither drop nor duplicate an element, because the gap that ends one chunk is the gap that begins the next. **The empty slice is expressible.** `s[a:a]` is empty. A closed-interval convention ("both ends included") cannot express an empty range without a special case, which is precisely the corner where off-by-one bugs breed. ### Negative indices Any negative index `i` is translated to `len(s) + i` before the slice is taken, so `-1` is the last element, `-2` the one before it, and `s[-3:]` reads as "the last three". This translation is a service the built-in sequences perform; it is not automatic in a class that implements `__getitem__` by hand, which must do the arithmetic itself. Negative indices compose with the half-open rule in the usual way: `s[:-1]` is "everything but the last", because the stop bound `len(s) - 1` is excluded. That reading is worth practising out loud, since it is the form most real code uses. ### Omitted bounds and the step An omitted `start` means the beginning, an omitted `stop` means the end, and `step` defaults to 1. The important subtlety is that the first two defaults are *direction-sensitive*: when `step` is negative, an omitted `start` means the **last** element and an omitted `stop` means "past the front". That is the whole explanation of `s[::-1]`: step backwards, starting at the end, running off the front — a reversed copy. A positive step skips: `s[::2]` is every other element from the start, `s[1::2]` every other element from position 1. Combining a step with bounds is legal (`s[1:8:3]`), and the count of items produced is `ceil((stop - start) / step)` rather than a simple subtraction. ### A slice produces a new object; an index produces an element `files[-1]` is the last element — a string, if the list holds strings. `files[-1:]` is a **list** containing one string. Confusing the two is one of the most common early bugs, and it shows up in error messages that look nonsensical (`AttributeError` on a list, a `TypeError` about concatenating a list to a string). The same asymmetry bites harder on `bytes`: slicing a `bytes` object gives `bytes`, but indexing one gives an `int`. Because a slice of a built-in sequence is a new object, `s[::-1]` never reverses in place. For a list, `list.reverse()` reverses the list itself and returns `None`, and the built-in `reversed()` gives a lazy iterator that copies nothing; `s[::-1]` is the option that costs a new sequence of the same length. For `str` and `tuple`, which are immutable, a copy is the only possibility. Building the slice is O(k) in the number of elements produced, so reversing a large list inside a hot loop is a real cost, not a free notation. ### The mental checklist When you read a slice, say four things to yourself: start is in, stop is out; negatives count from the end; missing bounds mean the ends, in the direction the step is going; and the result is a new sequence of the same type, never a view onto the original.

  • What does s[:k] + s[k:] equal for an arbitrary k, and why is that useful?
    It equals `s` for every `k`, including values past either end, because the two slices meet at exactly one gap. That is what makes fixed-size chunking loops correct without bounds checks: successive windows `s[0:n]`, `s[n:2*n]` and so on cover the sequence with no gap and no overlap, and the final short window falls out of the same expression.
  • Does slicing a list give you a view onto the original or a new list?
    A new list. The elements inside it are the same objects the original holds, but the list itself is separate, so appending to the slice or reordering it leaves the original untouched. Building it costs O(k) time and O(k) memory in the number of items produced, which matters when the slice is taken inside a hot loop over a large list.
  • How would you reverse a list without paying for a reversed copy?
    Use `list.reverse()` to reverse the list in place — it mutates and returns `None` — or the built-in `reversed()` when you only need to walk it backwards, since that yields a lazy iterator and allocates nothing proportional to the length. `s[::-1]` is the right choice when you specifically want a new sequence, and it is the only choice for `str` and `tuple`.

Think of a ruler laid along the sequence: you cut at the 1 cm and 4 cm marks and keep the piece between them. The piece is 3 cm long, and the second mark is where the cut is, not part of what you keep.

saying these in an interview costs you the question

  • Says the stop index is included in the result
  • Computes slice length as stop minus start plus one
  • Thinks a negative index is an error or wraps to the whole sequence
  • Believes s[::-1] reverses a list in place
  • Assumes a list slice is a view onto the original list
  • Confuses items[-1] with items[-1:] and expects the same type

context