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.
Slicing is syntax sugar over __getitem__
with a slice object. It 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.