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 ideaPython 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 endNegative 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] — reversedSlicing 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) # constructorAll 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
Indexing the list ['Chrome', 'Firefox', 'Safari', 'Edge']
| [0] | [1] | [2] | [3] | |
|---|---|---|---|---|
| Positive | 'Chrome' — first element | 'Firefox' | 'Safari' | 'Edge' — last element |
| Negative | [-4] → 'Chrome' | [-3] → 'Firefox' | [-2] → 'Safari' | [-1] → 'Edge' — common idiom for 'last' |
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 = originaldoes not create a new list. Mutatingcopymutatesoriginal. Useoriginal[:],original.copy(), orlist(original). - Confusing
appendandextend.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 returnsNone. Either callbrowsers.sort()and usebrowsers, or use the built-insorted(browsers)to get a new list.
🎯 Practice task
Wrangle a real test-result list. 25-30 minutes.
- Create
result_list.py. Define a listresultscontaining at least 8 dict items with keysname,status("PASS" / "FAIL"),duration_ms, andpriority("P0" / "P1" / "P2"). - Print
len(results)to confirm the count. - Use slicing to print only the first three results and only the last two.
- Use unpacking —
first_result, *rest = results— and printfirst_result["name"]andlen(rest). - Use
appendto add a new test result to the end. Useinsert(0, ...)to add another at the beginning. - Use a list comprehension (chapter 2) to build a list of just the failing test names.
- Use
sorted(results, key=lambda r: r["duration_ms"], reverse=True)to print the names of the three slowest tests. - Make a
copy = results[:], thencopy.pop(). Printlen(results)andlen(copy)— confirm only the copy shrank. - Stretch: sort
resultsby two keys at once — first bypriorityascending (P0 before P2) then byduration_msdescending. 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.