How does slicing work, and what's the difference between slicing a list, a string, and using `slice()`?
Quick Answer
`seq[start:stop:step]` returns a **new** sequence of the same type containing elements from `start` (inclusive) to `stop` (exclusive), stepping by `step`; negative indices count from the end and a negative `step` reverses direction. Under the hood, `a[start:stop:step]` builds a `slice` object and calls `a.__getitem__(slice(start, stop, step))` — the same mechanism for lists, strings, tuples, and any custom class implementing `__getitem__`.
Detailed Answer
The basic mechanics
s = "Hello, World!"
s[0:5] # 'Hello'
s[7:] # 'World!' -- omit stop -> to the end
s[:5] # 'Hello' -- omit start -> from the beginning
s[-6:] # 'World!' -- negative index counts from the end
s[::-1] # '!dlroW ,olleH' -- negative step reverses
s[::2] # 'Hlo ol!' -- every 2nd character
Slicing always returns a new object of the same type as the original
(a slice of a str is a str, a slice of a list is a list) — it never
mutates the original sequence, and out-of-range indices are clamped rather
than raising an error (unlike single-index access, which raises
IndexError).
list vs str slicing
Both use identical syntax and semantics, but a list slice creates a shallow copy of the sliced elements (the list itself is new, but if elements are mutable objects, they're the same objects, not deep copies):
nums = [1, 2, 3, 4, 5]
nums[1:3] = [20, 30] # slice assignment: replaces elements 1:3
nums # [1, 20, 30, 4, 5]
nums[1:3] = [7, 8, 9, 10] # can even change length!
nums # [1, 7, 8, 9, 10, 4, 5]
Strings are immutable, so s[1:3] = "x" raises TypeError — you can only
read a slice of a string, never assign into it; "modifying" a string means
building a new one (s = s[:1] + "X" + s[3:]).
The slice() object
a[start:stop:step] is syntax sugar for a.__getitem__(slice(start, stop, step)):
sl = slice(1, 5, 2)
"Hello, World!"[sl] # 'el' -- same as "Hello, World!"[1:5:2]
sl.start, sl.stop, sl.step # (1, 5, 2)
sl.indices(13) # normalizes negative/None values for a length-13 sequence
Storing a slice object as a variable is useful when the same slicing
pattern is reused in multiple places, or when a custom __getitem__
implementation needs to distinguish obj[i] (an int) from obj[i:j]
(a slice instance) to support both.
Interview-ready summary: Slicing is syntax sugar over __getitem__
with a slice object, works identically across strings, lists, and
tuples, always produces a new object of the source's type, and clamps
out-of-range bounds instead of raising. Lists additionally support slice
assignment (including changing length); strings, being immutable,
support slicing only for reading.