Lists — Storing and Manipulating Test Data

8 min read

The list is Python's most-used data structure — ordered, mutable, allows duplicates, and supports a generous toolbox of methods. In QA code, almost every collection of "things" you handle starts as a list: browsers to test, status codes seen in a run, test cases to execute, log lines parsed from a file. This lesson covers how to create, index, slice, modify, and copy lists, plus the unpacking syntax that turns a list into individual variables in one line.

Creating lists

Square brackets, comma-separated values:

browsers = ["Chrome", "Firefox", "Safari"]
status_codes = [200, 201, 404, 500]
empty = []
mixed = ["Chrome", 200, True, None]   # legal, but rarely a good idea

Python doesn't insist all elements share a type — mixed above is valid. In practice you should aim for homogeneous lists (all the same kind of thing). When elements truly differ, reach for a dict or a tuple instead.

Indexing — by position, including from the end

Lists are zero-indexed. The first element is [0], the last is [-1]:

browsers = ["Chrome", "Firefox", "Safari"]
print(browsers[0])    # "Chrome"
print(browsers[1])    # "Firefox"
print(browsers[-1])   # "Safari" — last element
print(browsers[-2])   # "Firefox" — second from end

Negative indices count back from the end. [-1] is the cleanest way to get the last item — much nicer than Java's list.get(list.size() - 1).

Indexing past the end raises IndexError: list index out of range. There is no silent undefined like in JavaScript; Python tells you immediately.

Slicing — [start:stop:step]

Slicing returns a new list containing a range of elements. The stop is exclusive (same convention as range):

nums = [10, 20, 30, 40, 50]
 
print(nums[1:3])     # [20, 30]      — index 1 up to but not including 3
print(nums[:2])      # [10, 20]      — start defaults to 0
print(nums[2:])      # [30, 40, 50]  — stop defaults to len(nums)
print(nums[:])       # [10, 20, 30, 40, 50]  — full copy
print(nums[::2])     # [10, 30, 50]  — every second element
print(nums[::-1])    # [50, 40, 30, 20, 10]  — reversed

Slicing never raises IndexError. nums[100:200] returns [], no exception. That's a deliberate design — slices are meant to be safe.

nums[:] is the canonical "shallow copy" idiom — much shorter than list(nums) though both work.

Modifying a list

Lists are mutable — you can change them in place. The methods you'll use daily:

browsers = ["Chrome", "Firefox", "Safari"]
 
browsers.append("Edge")              # add to the end
# ['Chrome', 'Firefox', 'Safari', 'Edge']
 
browsers.insert(0, "Brave")          # insert at index 0
# ['Brave', 'Chrome', 'Firefox', 'Safari', 'Edge']
 
browsers.remove("Safari")            # remove first matching value
# ['Brave', 'Chrome', 'Firefox', 'Edge']
 
last = browsers.pop()                # remove AND return the last element
# last == 'Edge'; browsers == ['Brave', 'Chrome', 'Firefox']
 
first = browsers.pop(0)              # remove AND return at index
# first == 'Brave'; browsers == ['Chrome', 'Firefox']
 
browsers.extend(["Opera", "Vivaldi"])  # append every element of another iterable
# ['Chrome', 'Firefox', 'Opera', 'Vivaldi']

A subtle point: append adds one element (even if it's a list — that nested list becomes one element). extend adds each element of an iterable. browsers.append(["A", "B"]) gives you [..., ["A", "B"]] while browsers.extend(["A", "B"]) gives you [..., "A", "B"].

Membership, length, and counting

browsers = ["Chrome", "Firefox", "Safari", "Chrome"]
 
len(browsers)                 # 4 — built-in function, not a method
"Chrome" in browsers          # True
"Edge" not in browsers        # True
browsers.count("Chrome")      # 2
browsers.index("Firefox")     # 1 — index of first match (raises if absent)

len() is a built-in (len(x)), not a method (x.length or x.length()). Different from JavaScript's arr.length and Java's list.size() — Python uses one universal function for "how big is this thing."

Sorting and reversing

Two flavours, depending on whether you want to keep the original:

numbers = [3, 1, 4, 1, 5, 9, 2, 6]
 
# In place — modifies numbers, returns None
numbers.sort()
print(numbers)        # [1, 1, 2, 3, 4, 5, 6, 9]
 
# Out of place — returns a new sorted list, leaves numbers untouched
original = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_copy = sorted(original)
print(sorted_copy)    # [1, 1, 2, 3, 4, 5, 6, 9]
print(original)       # unchanged
 
# Reverse sort
sorted(original, reverse=True)        # [9, 6, 5, 4, 3, 2, 1, 1]
 
# Sort by a key — by string length
words = ["short", "longer", "longest!"]
sorted(words, key=len)                # ['short', 'longer', 'longest!']
 
# Sort dicts by a field
results = [
    {"name": "login",    "duration": 1240},
    {"name": "checkout", "duration": 980},
]
sorted(results, key=lambda r: r["duration"])

The key= argument is a function that returns the value to sort by. We meet lambda (a one-line anonymous function) here for the first time — lambda r: r["duration"] is shorthand for def f(r): return r["duration"].

Unpacking — one line, several variables

Python can split a list into named pieces in a single statement:

browsers = ["Chrome", "Firefox", "Safari"]
first, second, third = browsers
print(first)    # "Chrome"
print(third)    # "Safari"

The element count must match — three names, three values. Use *rest to collect the leftovers:

first, *rest = browsers
print(first)    # "Chrome"
print(rest)     # ["Firefox", "Safari"]
 
first, *middle, last = browsers
print(middle)   # ["Firefox"]

*rest always becomes a list, even if there's nothing to collect. Unpacking is the cleanest way to peel off "the first item and the rest" — extremely common in test setup code.

Copying — be careful with =

This trips up everyone, eventually:

original = ["Chrome", "Firefox"]
copy = original           # NOT a copy — both names point at the same list
copy.append("Safari")
print(original)           # ['Chrome', 'Firefox', 'Safari']  — surprise!

copy = original makes a new name for the same list. Mutating one shows up in the other. To get a real (shallow) copy:

copy = original[:]         # slice of the whole thing
copy = original.copy()     # explicit method
copy = list(original)      # constructor

All three are equivalent and all three avoid the alias trap. (For nested lists, you'd need copy.deepcopy(...) — covered properly when we hit import in chapter 6.)

A QA example — managing a run's results

run_results = []
 
# Tests complete and report in
run_results.append({"name": "login",    "status": "PASS", "duration_ms": 1240})
run_results.append({"name": "checkout", "status": "FAIL", "duration_ms": 980})
run_results.append({"name": "search",   "status": "PASS", "duration_ms": 320})
run_results.append({"name": "logout",   "status": "PASS", "duration_ms": 410})
 
# How many tests ran?
print(f"Total: {len(run_results)}")
 
# Slice off the last failure for a quick re-run
last = run_results[-1]
print(f"Last test: {last['name']}")
 
# Pull just the failures (list comprehension from chapter 2)
failures = [r for r in run_results if r["status"] == "FAIL"]
print(f"Failures: {len(failures)}")
 
# Sort by duration, slowest first
slowest = sorted(run_results, key=lambda r: r["duration_ms"], reverse=True)
print(f"Slowest: {slowest[0]['name']} ({slowest[0]['duration_ms']} ms)")

Output:

Total: 4
Last test: logout
Failures: 1
Slowest: login (1240 ms)

That's a real scrap of test-runner code — append to collect, [-1] to peek, comprehension to filter, sorted with a key to rank.

A list, indexed two ways

Both rows reach the same element — the negative indices just count from the end. Use whichever reads more naturally for the case at hand.

⚠️ Common mistakes

  • Aliasing instead of copying. copy = original does not create a new list. Mutating copy mutates original. Use original[:], original.copy(), or list(original).
  • Confusing append and extend. browsers.append(["Edge", "Opera"]) adds a single element (the inner list). browsers.extend(["Edge", "Opera"]) adds each element separately. The fix is whichever your case actually needs.
  • sort() returns None. A common bug: sorted_browsers = browsers.sort(). The method modifies in place and returns None. Either call browsers.sort() and use browsers, or use the built-in sorted(browsers) to get a new list.

🎯 Practice task

Wrangle a real test-result list. 25-30 minutes.

  1. Create result_list.py. Define a list results containing at least 8 dict items with keys name, status ("PASS" / "FAIL"), duration_ms, and priority ("P0" / "P1" / "P2").
  2. Print len(results) to confirm the count.
  3. Use slicing to print only the first three results and only the last two.
  4. Use unpacking — first_result, *rest = results — and print first_result["name"] and len(rest).
  5. Use append to add a new test result to the end. Use insert(0, ...) to add another at the beginning.
  6. Use a list comprehension (chapter 2) to build a list of just the failing test names.
  7. Use sorted(results, key=lambda r: r["duration_ms"], reverse=True) to print the names of the three slowest tests.
  8. Make a copy = results[:], then copy.pop(). Print len(results) and len(copy) — confirm only the copy shrank.
  9. Stretch: sort results by two keys at once — first by priority ascending (P0 before P2) then by duration_ms descending. Hint: return a tuple from the lambda — key=lambda r: (r["priority"], -r["duration_ms"]).

You can now collect, slice, sort, and reshape any sequence Python gives you. The next lesson moves on to dicts — Python's tool for mapping keys to values, the structure that powers test config and JSON.

// tip to track lessons you complete and pick up where you left off across devices.