Python Data Structures
Master lists, tuples, dictionaries, sets, and comprehensions with practical production-ready examples.
Python ships with four essential built-in data structures that cover the majority of programming tasks: lists, tuples, dictionaries, and sets. Choosing the right structure for the job affects both the correctness and performance of your code.
Lists — Ordered, Mutable Sequences
Lists hold an ordered collection of items of any type and can be changed after creation. They’re the go-to structure when you need to maintain order, allow duplicates, and modify contents over time — appending results, sorting records, or building up a collection incrementally.
# Create a list of exam scores
scores = [95, 82, 78, 91, 88]
# Index and slice — same syntax as strings
print(scores[0]) # 95 — first element
print(scores[-1]) # 88 — last element
print(scores[1:3]) # [82, 78] — elements at index 1 and 2
# Mutate the list in place
scores.append(100) # add 100 to the end
scores.insert(0, 70) # insert 70 at index 0
scores.remove(78) # remove the first occurrence of 78
popped = scores.pop() # remove and return the last element
# Sort — sort() modifies in place, sorted() returns a new list
scores.sort(reverse=True) # descending, in-place
sorted_scores = sorted(scores) # ascending, new list — original unchanged
# Useful list methods
print(len(scores)) # number of elements
print(scores.count(82)) # how many times 82 appears
print(scores.index(91)) # index of the first occurrence of 91
List Comprehensions
List comprehensions create a new list by applying an expression to each element of an iterable, with an optional filter. They’re preferred over for loops with .append() because they’re more concise and compiled to faster bytecode.
numbers = range(1, 11)
# Apply a transformation to every element
squares = [n ** 2 for n in numbers]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# Add a filter condition — only include elements where the condition is True
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
# [4, 16, 36, 64, 100]
# Nested comprehension — flatten a 2D matrix into a 1D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
Tuples — Ordered, Immutable Sequences
Tuples behave like lists but cannot be modified after creation. Their immutability is their value: it signals to the reader that the data is fixed, it allows tuples to be used as dictionary keys, and it makes them slightly faster to create and access than lists.
# A 2D coordinate — makes no sense to change x without replacing the whole point
point = (10.5, 20.3)
x, y = point # tuple unpacking assigns each element to a variable
# Named tuples provide the immutability of a tuple with the readability of a class
from collections import namedtuple
Colour = namedtuple("Colour", ["red", "green", "blue"])
white = Colour(red=255, green=255, blue=255)
print(white.red) # 255 — access by name, not index
print(white) # Colour(red=255, green=255, blue=255)
# Tuples can be used as dictionary keys because they're hashable
grid = {(0, 0): "start", (3, 4): "end"}
print(grid[(0, 0)]) # "start"
Dictionaries — Key-Value Maps
Dictionaries store key-value pairs and provide O(1) average-time lookup. They’re the right choice when you need to associate pieces of data by a meaningful key rather than a positional index — user records, configuration objects, counters, and caches are all natural fits. Since Python 3.7, insertion order is preserved.
# Create a dict with string keys
user = {
"name": "Alice",
"age": 30,
"email": "[email protected]",
}
# Access — use .get() when the key might not exist
print(user["name"]) # "Alice" — KeyError if key missing
print(user.get("phone", "N/A")) # "N/A" — safe access with a default
# Mutate
user["age"] = 31 # update an existing key
user["role"] = "admin" # add a new key
del user["email"] # remove a key entirely
# Iterate over key-value pairs
for key, value in user.items():
print(f"{key}: {value}")
# Dict comprehension — build a mapping from an iterable
word = "hello"
char_count = {char: word.count(char) for char in set(word)}
# {'h': 1, 'e': 1, 'l': 2, 'o': 1}
Merging Dictionaries (Python 3.9+)
The | operator merges two dicts into a new one. Keys from the right-hand dict override keys from the left, which makes it natural for applying user overrides on top of defaults.
defaults = {"theme": "dark", "lang": "en"}
overrides = {"lang": "fr", "timezone": "UTC"}
# Right side wins on conflicts — "lang" becomes "fr"
merged = defaults | overrides
# {'theme': 'dark', 'lang': 'fr', 'timezone': 'UTC'}
Sets — Unordered, Unique Collections
Sets automatically deduplicate their contents and provide O(1) membership testing — the same speed as a dict lookup, much faster than scanning a list. They’re the right choice when you need uniqueness guarantees or want to compute relationships between groups of items (intersection, union, difference).
# Duplicate "python" is removed automatically on creation
tags = {"python", "web", "api", "python"}
print(tags) # {'python', 'web', 'api'} — order not guaranteed
# Membership test — O(1), regardless of set size
print("python" in tags) # True
# Set operations mirror mathematical set theory
backend = {"python", "django", "postgres"}
frontend = {"javascript", "react", "python"}
shared = backend & frontend # intersection: {'python'}
all_tech = backend | frontend # union: all unique items from both
only_backend = backend - frontend # difference: {'django', 'postgres'}
either_not_both = backend ^ frontend # symmetric difference
# Add and remove elements
tags.add("backend")
tags.discard("api") # discard() won't raise an error if the element is missing
Choosing the Right Structure
Picking the wrong data structure usually shows up as slow code or awkward access patterns. This table summarizes the key trade-offs:
| Use case | Best type |
|---|---|
| Ordered, changeable collection | list |
| Fixed record (coordinates, config row) | tuple |
| Fast key-value lookup | dict |
| Membership tests, deduplication | set |
| Immutable set (used as a dict key) | frozenset |
The most common mistake is using a list for membership testing when the collection is large. Searching a list is O(n); a set lookup is O(1). If you’re writing if item in my_list inside a loop, convert my_list to a set first.