Accessing and Modifying List Elements

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.

ToolPurpose
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