Skip to content
CodeKruPro

Your language · Python

Collections: lists, sets and maps

The built-in collections, when to use each, and how they behave.

Learn: Python collections: lists, tuples, dicts and sets9 minute read

A collection holds a group of values. Python has four built-in ones, and they cover nearly every need in test code: a list of elements found on a page, tuples of test parameters, a dict for a JSON body (structured data sent to a web service) or headers (extra information sent with a web request), and a set for uniqueness checks. Interviewers ask when to use each one, how a dict finds a key, and about a few classic traps with copies and loops.

Step 1: the four built-ins

TypeOrderedCan changeDuplicatesTypical use
list [1, 2]YesYesYesElements on a page, column values
tuple (1, 2)YesNoYesFixed records, parametrize rows, locators
dict {"k": 1}Insertion orderYesKeys uniqueJSON bodies, headers, counts
set {1, 2}NoYesNoUniqueness, comparing without order

Two syntax traps: {} is an empty dict (an empty set is set()), and (1) is just the number 1 (a one-item tuple needs a comma: (1,)). Since Python 3.7, dicts are guaranteed to keep insertion order (the order in which you added the keys). In the table, "can change" means you can add, remove or replace items after creating it.

Step 2: how a dict finds a key

A dict is a hash table. To store a key, Python calls hash(key) (a function that turns the key into a number) to choose a slot (a numbered place) in an internal table. If the slot is taken, it tries other slots, and it matches keys by hash and then by ==. Assigning to an existing key replaces the value and keeps the key's position. On average, a lookup takes the same short time however big the dict is (written O(1)), so key in some_dict and x in some_set are much faster than x in some_list on large data.

Keys must be hashable, which means their hash can never change. Strings, numbers and tuples work; lists and dicts do not. A set is the same structure with keys only.

links = ["Home", "Login", "Home", "Cart", "Login"]
print(len(set(links)), sorted(set(links)))
print(list(dict.fromkeys(links)))            # unique, page order kept

counts = {}
for link in links:
    counts[link] = counts.get(link, 0) + 1
print(counts)

print(counts.get("Help"), counts.get("Help", 0))
try:
    counts["Help"]
except KeyError as e:
    print("KeyError:", e)

by_count = dict(sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])))
print(by_count)

expected_options = {"India", "Japan", "Peru"}
actual_options = {"Peru", "India", "Chile"}
print("missing:", expected_options - actual_options, "extra:", actual_options - expected_options)

try:
    {["a"]: 1}
except TypeError as e:
    print("TypeError:", e)
3 ['Cart', 'Home', 'Login']
['Home', 'Login', 'Cart']
{'Home': 2, 'Login': 2, 'Cart': 1}
None 0
KeyError: 'Help'
{'Home': 2, 'Login': 2, 'Cart': 1}
missing: {'Japan'} extra: {'Chile'}
TypeError: unhashable type: 'list'

Set subtraction is a quick way to report which dropdown options are missing or extra.

Step 3: lists, sorting and safe loops

A list is a dynamic array of references (a row of memory that grows as needed and points at the objects). Reading by index and adding at the end are fast; inserting or removing at the front is slow because every item shifts, so use collections.deque for a queue. lst.sort() sorts in place and returns None, while sorted(lst) returns a new list. Both take a key function (it tells sort what to compare) instead of a comparator: sorted(users, key=lambda u: (u["role"], u["name"])). A lambda is a short function written in one line; see Lambdas and comprehensions. To check that a column on a page is sorted, compare it with sorted(values).

Never remove items from a list while you loop over it: the loop silently skips the item after each removal. Build a new list with a comprehension instead (a one-line loop that builds a list, such as [n for n in nums if n % 2]). Changing a dict's size while looping over it raises RuntimeError (a general error while the program runs).

Step 4: copies and the collections module

Assignment never copies: b = a gives one list two names. a.copy(), a[:] and list(a) make a shallow copy, which is a new outer list that still shares the inner objects. copy.deepcopy copies every level, which you need before changing nested data, such as a JSON template (a ready-made example of a request body).

import copy
from collections import Counter, defaultdict, deque

template = {"user": {"name": "Asha", "roles": ["viewer"]}}
shallow = copy.copy(template)
deep = copy.deepcopy(template)
template["user"]["roles"].append("admin")
print(shallow["user"]["roles"], deep["user"]["roles"])

grid = [[0] * 2] * 2
grid[0][0] = 1
print(grid)
grid = [[0] * 2 for _ in range(2)]
grid[0][0] = 1
print(grid)

nums = [1, 2, 2, 3, 4]
nums = [n for n in nums if n % 2]
print(nums)

print(Counter("banana").most_common(2))
by_team = defaultdict(list)
for name, team in [("Asha", "QA"), ("Ravi", "Dev"), ("Bea", "QA")]:
    by_team[team].append(name)
print(dict(by_team))
recent = deque(maxlen=3)
for line in ["l1", "l2", "l3", "l4"]:
    recent.append(line)
print(list(recent))
['viewer', 'admin'] ['viewer']
[[1, 0], [1, 0]]
[[1, 0], [0, 0]]
[1, 3]
[('a', 3), ('n', 2)]
{'QA': ['Asha', 'Bea'], 'Dev': ['Ravi']}
['l2', 'l3', 'l4']

[[0] * 2] * 2 repeats one inner list, so changing one row changed both. The collections module adds Counter for counting, defaultdict for grouping, deque for fast queues (with maxlen to keep the last N items) and namedtuple for small records.

How to answer in an interview

Compare the types on the same few points: order, whether they can change, duplicates, and lookup speed. Then give a test use for each: "find_elements returns a list; I pass (user, password) tuples to parametrize (a pytest feature that runs one test with several rows of data); the response body is a dict; I use a set to prove IDs are unique." For "how does a dict work?", walk through hashing the key, choosing a slot and matching with ==.

Common mistakes

  • Writing {} for an empty set, or (1) for a one-item tuple.
  • Writing x = lst.sort() and getting None.
  • Removing items from a list inside a loop over the same list.
  • Changing a "copy" made with b = a, or a shallow copy of nested data.
  • Using d["key"] when the key may be missing; d.get("key") returns None instead of raising KeyError (the error Python gives for a missing key).

Recap

Use a list for ordered data, a tuple for fixed records, a dict for key lookups and a set for uniqueness. Dicts and sets are hash tables, so keys must be hashable and lookups are fast. Sort with a key function, filter into a new list instead of removing in a loop, and use deepcopy before changing nested data.

11 questions0/11 solved

StatusQuestionDifficultyAsked in
Not startedRemove duplicates but keep the orderEasyAsked in: Accenture, Capgemini, Cognizant +3
Not startedlist vs tuple vs dict vs setEasyAsked in: Accenture, Capgemini, Infosys +2
Pro+How does a dict work internally?MediumAsked in: Accenture, Capgemini, Cognizant +1
Pro+Lists, arrays and dequesMediumAsked in: Accenture, Capgemini, Infosys +2
Pro+What can be a dict key?Easy-Medium
Pro+Changing a collection while looping over itMediumAsked in: Accenture, Infosys, TCS
Pro+Ways to loop over a dictEasy-MediumAsked in: Accenture, Infosys, TCS
Pro+Sorting with key functions; sorting a dict by valueMediumAsked in: Accenture, Infosys
Pro+Aliases, shallow copies and the [[0] * 2] * 2 trapMedium
Pro+Counter, defaultdict, deque and namedtupleEasy-Medium
Pro+sort() versus sorted()Easy-MediumAsked in: Accenture, Infosys
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan