Reaching into a sequence: indexes, slices and why the end is excluded
Counting starts at zero
items = ["rice", "dal", "oil", "salt"]
items[0] # 'rice'
items[3] # 'salt'
items[4] # IndexError: list index out of rangeFour items, valid positions 0 to 3. The last position is always len(items) - 1, and asking for len(items) itself is the single most common IndexError.
Negative numbers count from the right:
items[-1] # 'salt' — last
items[-2] # 'oil' — second from lastitems[-1] is worth a habit. It means "the last one" without computing a length, and it keeps working when the list changes size.
A slice takes a range
items[1:3] # ['dal', 'oil']
items[:2] # ['rice', 'dal'] from the start
items[2:] # ['oil', 'salt'] to the end
items[:] # a full copyThe first number is where to start, the second is where to stop before. items[1:3] gives positions 1 and 2, not 1 to 3.
People find this arbitrary. It is not, and two properties explain why it was chosen:
- The length of a slice is
stop - start.items[2:5]has 3 elements. No arithmetic, no off-by-one. items[:k] + items[k:]reconstructs the original, for any k. The cut point belongs to exactly one side. With an inclusive end you would needitems[:k] + items[k+1:]and you would drop an element every time you forgot.
Once you have written a few loops that split a list at a moving point, the convention stops feeling odd.
The step, and reversing
A third number is the step:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers[::2] # [0, 2, 4, 6, 8] every second
numbers[1::2] # [1, 3, 5, 7, 9] every second from position 1
numbers[::-1] # [9, 8, 7, ..., 0] reversed[::-1] is the standard idiom for reversing a list or a string. It builds a new reversed copy; list.reverse() reverses in place and returns nothing.
The difference that saves you from crashes
Indexing out of range raises. Slicing out of range does not.
short = ["a", "b"]
short[5] # IndexError
short[1:99] # ['b']
short[10:20] # [] — an empty list, no errorThis is genuinely useful. Taking "the first ten results" with results[:10] is safe whether there are 3 results or 3,000. Writing results[0] on a possibly-empty list is not, and if results: has to guard it.
Given a search or an API that may return nothing, results[:1] gives you a list with zero or one item, and a loop over it does the right thing in both cases without an if.
Slicing strings works the same way
code = "ADD-2026-0147"
code[:3] # 'ADD'
code[4:8] # '2026'
code[-4:] # '0147'But strings are immutable — you cannot assign into one:
code[0] = "X" # TypeError: 'str' object does not support item assignmentYou build a new string instead: "X" + code[1:]. Lists do allow assignment, including to a whole slice:
items[1:3] = ["atta"] # replaces two items with one; the list shrinksThat last form surprises people. Slice assignment does not have to be the same length.
A slice of a list is a shallow copy
a = [1, 2, 3]
b = a[:]
b.append(4)
a # [1, 2, 3] — untoucheda[:] was the standard way to copy a list before list(a) and a.copy() existed. All three do the same thing, and all three are shallow: if the list contains other lists, both copies point at the same inner lists. The next lesson is about exactly that.
Slices on strings copy; slices on big data may not
For lists and strings, a slice allocates a new object, so big_list[:] on a million items costs a million pointer copies. Inside a loop that is a real cost people miss. NumPy arrays and pandas frames, which arrive later in this course, behave differently — a NumPy slice is a view onto the original memory, and writing to it changes the original. Two libraries, two rules; the course flags it again when you get there.
Try this now
Take "2026-09-04" and extract the year, the month and the day with slices. Then take a list of ten numbers and produce, with slices only: the first three, the last three, every alternate one, and the whole thing backwards. Four one-liners, no loops.
The one thing to keep
A slice stops before its second index, which makes its length exactly stop minus start, and slicing out of range returns a shorter result instead of raising.
Before you move on
A function returns the top three matches with `results[:3]`. A colleague argues this will crash when the search finds only one match and rewrites it as `results[0:3] if len(results) >= 3 else results`. What is true?
Pick the one you would defend. Nobody sees your answer.