How does slicing work, and what's the difference between slicing a list, a string, and using `slice()`?

5 minbeginnerfundamentalsslicingsequences

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.