Safe List Access

Concepts covered: List Modification

Reading an index that doesn't exist, popping from an empty list and removing a value that is not there all stop a program. Each has a check that costs 1 line and runs before the risky call, so the program prints a message and carries on instead of ending with a traceback.

Check the length before indexing

A list of n items accepts indexes 0 through n - 1. An index is safe when it's below len(items), and, for counting from the front, at least 0. Compare first, read second.

readings = [20.5, 21.0]
wanted = 5
if wanted < len(readings):
    print(readings[wanted])
else:
    print("no reading at", wanted)
print("still running")

Output:

no reading at 5
still running

The else branch prints a message and the program reaches the last line. Without the if, readings[5] would have raised IndexError and "still running" would never appear. The same guard works inside a loop, where and joins the 2 halves of the range check.

temps = [18, 21, 19]
for wanted in [0, 2, 3, -1]:
    if wanted >= 0 and wanted < len(temps):
        print(wanted, "->", temps[wanted])
    else:
        print(wanted, "-> skipped")

Output:

0 -> 18
2 -> 19
3 -> skipped
-1 -> skipped

This guard treats negative numbers as invalid on purpose. Python would accept temps[-1], but a position that arrives from a form or a counter means a count from the front, and a -1 there is a mistake worth catching rather than a request for the last item.

Check for empty before pop

A loop that pops on every pass needs the list to still have items. Testing len(inbox) > 0 before each pop turns the third and fourth passes into a message instead of a crash.

inbox = ["bill", "invite"]
for step in range(4):
    if len(inbox) > 0:
        print("read:", inbox.pop())
    else:
        print("inbox empty")

Output:

read: invite
read: bill
inbox empty
inbox empty

The 2 pops empty the inbox, and from then on the else branch answers. All 4 passes complete, and the program never reaches a pop on an empty list.

Count before remove

The count method returns how many times a value appears in the list. A count of 0 means remove would raise ValueError, so test the count first and only remove when it is above 0.

guests = ["Ana", "Bo", "Ana"]
print(guests.count("Ana"))
print(guests.count("Cy"))
if guests.count("Cy") > 0:
    guests.remove("Cy")
else:
    print("Cy was never on the list")
print(guests)

Output:

2
0
Cy was never on the list
['Ana', 'Bo', 'Ana']

Guarding the first and last item

items[0] and items[-1] name positions every non-empty list has, but an empty list has neither a first nor a last item, so both raise IndexError on []. The guard is the same emptiness test used before pop: check the length, then read.

latest = []
if len(latest) > 0:
    print("newest:", latest[-1])
else:
    print("no readings yet")
latest.append(22.4)
if len(latest) > 0:
    print("newest:", latest[-1])

Output:

no readings yet
newest: 22.4

Wrap it in a function

A function that returns a fallback value when the index is out of range lets the rest of a program read positions without an if at every call site. The caller always gets a value back: the item when it exists, the fallback when it does not.

def safe_get(items, index, fallback):
    if index >= 0 and index < len(items):
        return items[index]
    return fallback

queue = ["intro", "verse", "chorus"]
print(safe_get(queue, 1, "silence"))
print(safe_get(queue, 3, "silence"))
print(safe_get(queue, -1, "silence"))
verse
silence
silence

Index 1 passes both halves of the test and "verse" comes back. Index 3 fails the length check and index -1 fails the sign check, so each returns "silence" and the program prints 3 lines with no traceback.

The caller picks the fallback, so each call site decides what missing looks like: 0 for a count, an empty string for a label, "silence" for an empty slot in a playlist. The function only decides whether the index is in range, which keeps it usable for a list of any type.

Do
  • Compare the index against len() before reading from the list
  • Test len(items) > 0 before every pop()
  • Test count(value) > 0 before remove(value)
  • Return a fallback from a helper so callers always get a value
Avoid
  • Assume a list still has items inside a loop that pops
  • Hard-code an index like 3 without checking the length
  • Call remove on a value you never confirmed is present
  • Let a number from outside the program reach [] unchecked

What does this program print?

parts = ["cpu", "ram"]
if len(parts) > 2:
    print(parts[2])
else:
    print(parts[-1])

About this lesson

This section is part of the Lists: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.