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.

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.