Negative indices count from the right
s[-1] # last item
s[-3:] # last three
s[:-2] # everything except the last two
-1 is the last element, not "one before the start". Mixing the two conventions is fine: s[2:-1] is "from index 2 to the second-to-last".
The third value is the step
s[::2] # every second item
s[1::2] # every second, starting at 1
s[::-1] # reversed
[::-1] is the standard reverse idiom and worth memorising as one symbol rather than parsing each time. A negative step walks backwards, so start and stop swap roles — which is why s[5:2:-1] gives you something and s[2:5:-1] gives you nothing.
Slicing forgives, indexing does not
s[2:99] # fine, gives what exists
s[99] # IndexError
A slice clamps to the available range and returns what it can, including an empty result. That is convenient and occasionally hides a bug, because an empty slice looks like valid data rather than a mistake.
Slices copy, and assignment mutates
A slice of a list is a new list, so changing it leaves the original alone. But assigning into a slice changes the original in place, and can change its length:
nums[1:3] = ["a", "b", "c"]
replaces two items with three. That is a genuine feature and a genuine surprise.
Reversing three ways
data[::-1] builds a new reversed list. reversed(data) returns a lazy iterator and copies nothing. data.reverse() reorders in place and returns None — the same trap as .sort().