Once a list exists, almost everything you do with it is indexed by position. Python uses square brackets for both reading (items[2]) and writing (items[2] = "new"), with negative indexes counting from the end: items[-1] is the last element. Understanding the index rules — and, just as important, what happens when an index is out of range — keeps you out of the most common list bugs.
Slicing extends indexing to a range of positions. items[a:b] returns a new list from index a up to but not including b. You can also provide a step: items[::2] takes every other element, items[::-1] reverses the list. Slices used on the left-hand side of an assignment replace that whole region: items[1:3] = ["x"] removes two items and inserts one in their place.
Modifying a list always means choosing between in-place changes (which alter the existing list) and rebinding (which leaves the original intact and creates a new list). items.append(x), items[0] = x, and del items[0] are in-place. items + [x] and sorted(items) are not. Mixing them up is a major source of subtle bugs, especially inside loops.
Finally, read-after-write and write-during-iteration are two patterns to treat with care. Python will not stop you from modifying a list while you iterate over it, but the results are usually wrong. The safe recipe is to iterate over a copy (for x in items[:]:) or to build a new list and replace the old one afterwards.
Indexing, slicing and assignment
Single-element access raises IndexError when the index is past the ends. Slicing never raises: items[100:200] on a 5-item list just returns []. This asymmetry is deliberate — it lets you write pagination and windowing code without checking bounds.
When you assign to a slice you can insert (items[2:2] = ["x"]), replace (items[1:3] = ["x", "y"]), or delete (items[1:3] = []). The assigned iterable does not need to match the slice length, but it must be iterable.
Removing items the right way
list.pop() removes and returns the last item; list.pop(0) removes from the front (O(n)). list.remove(value) finds the first occurrence and deletes it, raising ValueError if not found. del items[i] deletes by index. Use whichever makes your intent clearest.
A common bug: for i, x in enumerate(items): if cond(x): del items[i]. This skips elements after each deletion. Build a new list with a comprehension instead: items = [x for x in items if not cond(x)].
These are the access and modification tools that come up on every list.
| Tool | Purpose |
|---|---|
items[i]operator | Returns the item at index i (supports negative indexing). |
items[a:b:s]slicing | Returns a new list of items between a and b with step s. |
list.pop(i=-1)method | Removes and returns the item at index i (default: last). |
list.remove(x)method | Removes the first item equal to x (raises ValueError if absent). |
del items[i]statement | Deletes the item at index i from the list. |
reversed(seq)built-in | Returns an iterator over the list in reverse order. |
enumerate(seq)built-in | Yields (index, item) pairs for readable loops. |
list.index(x)method | Returns the index of the first occurrence of x. |
Accessing and Modifying List Elements code example
The script below demonstrates indexing, slicing, replacement, and safe deletion while iterating.
# Lesson: Accessing and Modifying List Elements
letters = ["a", "b", "c", "d", "e", "f"]
print("first, last:", letters[0], letters[-1])
print("middle: ", letters[2:5])
print("every 2nd: ", letters[::2])
print("reversed: ", letters[::-1])
letters[1] = "B"
letters[2:4] = ["C1", "C2", "C3"] # replace 2 with 3
print("after edits:", letters)
letters.insert(0, "start")
last = letters.pop()
letters.remove("B")
print("shaped: ", letters, "(popped", last, ")")
data = [1, 2, 3, 4, 5, 6, 7, 8]
data = [n for n in data if n % 2 == 0]
print("kept evens: ", data)
for i, v in enumerate(data, start=1):
print(f" #{i}: {v}")
What to notice in the script:
1) Negative indexing gives a concise way to say 'last' without computing len().
2) `letters[2:4] = [...]` can replace a slice with a different-length list.
3) Rebuilding the list with a comprehension is the safe way to filter while iterating.
4) `enumerate` is preferred over tracking an index manually in a for-loop.
Try both snippets to practice slice assignment and safe deletion.
# Example A: insert several items at once
nums = [1, 2, 3, 4]
nums[2:2] = [10, 20]
print(nums) # [1, 2, 10, 20, 3, 4]
# Example B: remove duplicates while preserving order
raw = ["a", "b", "a", "c", "b"]
seen: set[str] = set()
result = [x for x in raw if not (x in seen or seen.add(x))]
print(result)
These assertions check each operation's behavior.
xs = [0, 1, 2, 3]
assert xs[-1] == 3 and xs[1:3] == [1, 2]
xs[1:3] = ["new"]
assert xs == [0, "new", 3]
xs.append(9); assert xs[-1] == 9
assert xs.pop(0) == 0 and xs[0] == "new"
The script prints:
first, last: a f
middle: ['c', 'd', 'e']
every 2nd: ['a', 'c', 'e']
reversed: ['f', 'e', 'd', 'c', 'b', 'a']
after edits: ['a', 'B', 'C1', 'C2', 'C3', 'e', 'f']
shaped: ['start', 'a', 'C1', 'C2', 'C3', 'e'] (popped f )
kept evens: [2, 4, 6, 8]
#1: 2
#2: 4
#3: 6
#4: 8