Data Structures
Comprehensive guide on Data Structures.
Data Structures
Overview#
Python provides several built-in data structures for storing and organizing
collections of data: lists, tuples, dictionaries, and sets, plus specialized
containers in the collections module. Each has different characteristics
around mutability, ordering, performance, and intended use, which makes
choosing the right one an important skill for writing clean, efficient
code. This notebook covers all of the core structures in depth — how they
work internally, their methods, when to use each one, comprehensions for
building them concisely, and common pitfalls beginners run into.
1. Lists#
A list is an ordered, mutable collection that can hold items of any type, including a mix of types.
Python 3.12 (WASM)fruits = ["apple", "banana", "cherry"]
mixed = [1, "two", 3.0, True, [4, 5]]
# Accessing elements
print(fruits[0]) # apple
print(fruits[-1]) # cherry (last item)
# Slicing: list[start:stop:step]
print(fruits[0:2]) # ['apple', 'banana']
print(fruits[::-1]) # ['cherry', 'banana', 'apple'] (reversed)
print(fruits[::2]) # every second item
# Modifying
fruits.append("date")
fruits.insert(1, "avocado")
fruits.remove("banana") # removes first match by value
fruits[0] = "apricot" # replace by index
print(fruits)
Common list methods#
| Method | Description |
|---|---|
append(x) | Add item to the end |
insert(i, x) | Insert item at index i |
remove(x) | Remove first matching item (raises ValueError if absent) |
pop(i) | Remove and return item at index i (default: last) |
sort(key=None, reverse=False) | Sort in place |
sorted(list) | Return a new sorted list, original unchanged |
reverse() | Reverse in place |
index(x) | Find index of first match (raises ValueError if absent) |
count(x) | Count occurrences of x |
extend(iterable) | Append all items from another iterable |
clear() | Remove all items |
copy() | Shallow copy of the list |
Python 3.12 (WASM)numbers = [5, 3, 8, 1]
numbers.sort()
print(numbers) # [1, 3, 5, 8]
print(numbers.pop()) # 8
print(numbers) # [1, 3, 5]
# Sorting with a key function
people = [("Alice", 30), ("Bob", 25), ("Cara", 35)]
people.sort(key=lambda p: p[1])
print(people) # [('Bob', 25), ('Alice', 30), ('Cara', 35)]
How lists work internally#
A Python list is a dynamic array: it stores references to objects in a
contiguous, resizable block of memory. This makes indexing (lst[i]) and
appending to the end very fast — O(1) on average — while inserting or
removing from the beginning of a list is slower — O(n) — because every
following element has to shift.
Python 3.12 (WASM)# Fast: append to the end
lst = []
for i in range(1000):
lst.append(i) # O(1) amortized per call
# Slow for large lists: inserting at the front shifts everything
lst.insert(0, -1) # O(n)
Shallow copy vs deep copy#
Python 3.12 (WASM)original = [[1, 2], [3, 4]]
shallow = original.copy() # or original[:]
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — inner list is shared!
import copy
deep = copy.deepcopy(original)
deep[0].append(100)
print(original) # unaffected
A shallow copy duplicates the outer list, but nested mutable objects (like
inner lists) are still shared references. Use copy.deepcopy() when you
need a fully independent copy of nested structures.
2. Tuples#
A tuple is like a list, but immutable — once created, its contents cannot be reassigned. Tuples are typically used for fixed collections of related values, or when you want to guarantee data won't be modified.
Python 3.12 (WASM)point = (3, 4)
person = ("Alice", 30, "Engineer")
single = (5,) # note the trailing comma — required for a 1-item tuple
x, y = point # unpacking
print(x, y) # 3 4
name, age, *rest = ("Bob", 28, "Engineer", "Remote")
print(name, age, rest) # Bob 28 ['Engineer', 'Remote']
# Tuples cannot be modified
# point[0] = 10 # TypeError: 'tuple' object does not support item assignment
Why use a tuple over a list?#
- Signals intent that the data should not change — self-documenting code.
- Slightly faster to create and more memory-efficient than lists.
- Hashable (as long as all elements are hashable), so tuples can be used as dictionary keys or set members — lists cannot, since they're mutable.
- Commonly used as lightweight, unnamed records: a coordinate, an RGB triple, a database row.
Python 3.12 (WASM)locations = {
(28.6139, 77.2090): "Delhi",
(19.0760, 72.8777): "Mumbai",
}
print(locations[(28.6139, 77.2090)]) # Delhi
namedtuple for readable tuples#
When a tuple represents a record with meaningful fields, namedtuple
gives you attribute access without the overhead of a full class.
Python 3.12 (WASM)from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p) # Point(x=3, y=4)
3. Dictionaries#
A dictionary stores key-value pairs. Keys must be unique and hashable (strings, numbers, tuples of hashable items); values can be anything.
Python 3.12 (WASM)student = {
"name": "Rohan",
"age": 21,
"courses": ["Math", "Physics"]
}
print(student["name"]) # Rohan
student["age"] = 22 # update
student["email"] = "r@x.com" # add new key
# Safe access with default (no KeyError)
print(student.get("grade", "N/A")) # N/A
# Removing keys
del student["email"]
popped_value = student.pop("age")
Common dictionary methods#
| Method | Description |
|---|---|
keys() | View of all keys |
values() | View of all values |
items() | View of key-value pairs |
get(k, default) | Safe lookup with fallback |
pop(k, default) | Remove key and return its value |
setdefault(k, default) | Return value if key exists, else set and return default |
update(other) | Merge another dict (or iterable of pairs) in |
clear() | Remove all items |
Python 3.12 (WASM)for key, value in student.items():
print(f"{key}: {value}")
# setdefault avoids a separate existence check
counts = {}
for word in ["a", "b", "a", "c", "b", "a"]:
counts[word] = counts.get(word, 0) + 1
print(counts) # {'a': 3, 'b': 2, 'c': 1}
defaultdict and Counter#
The collections module offers specialized dictionary variants that
simplify common patterns.
Python 3.12 (WASM)from collections import defaultdict, Counter
# defaultdict: auto-initializes missing keys
groups = defaultdict(list)
for word in ["apple", "banana", "avocado", "blueberry"]:
groups[word[0]].append(word)
print(dict(groups)) # {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry']}
# Counter: purpose-built for counting
counts = Counter(["a", "b", "a", "c", "b", "a"])
print(counts) # Counter({'a': 3, 'b': 2, 'c': 1})
print(counts.most_common(2)) # [('a', 3), ('b', 2)]
Dictionary ordering#
Since Python 3.7, dictionaries preserve insertion order as a language
guarantee, not just an implementation detail. This means iterating over
.items() will always reflect the order keys were added.
4. Sets#
A set is an unordered collection of unique, hashable items. Sets are optimized for fast membership testing and eliminating duplicates.
Python 3.12 (WASM)colors = {"red", "green", "blue"}
colors.add("yellow")
colors.discard("red") # no error if missing
# colors.remove("red") # would raise KeyError if missing
print("green" in colors) # True — O(1) average lookup
# Deduplicate a list while losing order
numbers = [1, 2, 2, 3, 3, 3]
unique = set(numbers)
print(unique) # {1, 2, 3}
# Deduplicate while PRESERVING order (Python 3.7+ dict trick)
ordered_unique = list(dict.fromkeys(numbers))
print(ordered_unique) # [1, 2, 3]
Set operations#
Python 3.12 (WASM)a = {1, 2, 3}
b = {2, 3, 4}
print(a | b) # union: {1, 2, 3, 4}
print(a & b) # intersection: {2, 3}
print(a - b) # difference: {1}
print(a ^ b) # symmetric difference: {1, 4}
print(a.issubset({1, 2, 3, 4})) # True
print(a.issuperset({1, 2})) # True
frozenset#
A frozenset is an immutable version of a set — hashable, so it can be
used as a dictionary key or nested inside another set.
Python 3.12 (WASM)fs = frozenset([1, 2, 3])
# fs.add(4) # AttributeError: frozenset has no add method
5. Choosing the Right Structure#
| Structure | Ordered | Mutable | Duplicates | Lookup by | Typical use |
|---|---|---|---|---|---|
| List | Yes | Yes | Yes | Index | General-purpose sequence |
| Tuple | Yes | No | Yes | Index | Fixed, related values |
| Dictionary | Yes (3.7+) | Yes | Keys unique | Key | Key-based lookup |
| Set | No | Yes | No | Value (membership) | Membership tests, dedup |
| frozenset | No | No | No | Value | Immutable set, hashable |
6. Comprehensions#
Comprehensions provide a concise, often faster syntax for building lists, dictionaries, and sets from an iterable, replacing verbose manual loops.
List comprehension#
Python 3.12 (WASM)squares = [n ** 2 for n in range(10)]
evens = [n for n in range(20) if n % 2 == 0]
# Nested comprehension: flatten a list of lists
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [n for row in matrix for n in row]
print(flat) # [1, 2, 3, 4, 5, 6]
Dictionary comprehension#
Python 3.12 (WASM)word_lengths = {word: len(word) for word in ["apple", "kiwi", "banana"]}
# {'apple': 5, 'kiwi': 4, 'banana': 6}
# Invert a dictionary
original = {"a": 1, "b": 2}
inverted = {v: k for k, v in original.items()}
# {1: 'a', 2: 'b'}
Set comprehension#
Python 3.12 (WASM)unique_lengths = {len(word) for word in ["apple", "kiwi", "banana"]}
# {5, 4, 6}
Generator expressions#
A generator expression looks like a list comprehension but with parentheses, and produces items lazily — one at a time — instead of building the entire collection in memory at once.
Python 3.12 (WASM)total = sum(n ** 2 for n in range(1_000_000)) # never builds a full list
When to prefer a comprehension vs a loop#
Python 3.12 (WASM)# Instead of:
result = []
for n in range(10):
if n % 2 == 0:
result.append(n * n)
# Prefer:
result = [n * n for n in range(10) if n % 2 == 0]
Comprehensions should stay readable — if a comprehension needs multiple
nested loops or complex conditions, a regular for loop with clear
variable names is often the better, more maintainable choice.
7. Mutability and Identity Pitfalls#
The mutable default argument trap#
Python 3.12 (WASM)# Dangerous: the same list is reused across all calls!
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] <- unexpected!
# Correct approach
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Equality vs identity#
Python 3.12 (WASM)a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True (same contents)
print(a is b) # False (different objects in memory)
c = a
print(a is c) # True (same object)
Notes#
- Lists and dictionaries are the two most commonly used structures in everyday Python code.
- Use a tuple when data should not change after creation, or when you need a hashable, fixed-size grouping.
- Use a set when you only care about uniqueness and membership, not order or duplicates.
collections.defaultdictandcollections.Countersolve extremely common patterns (grouping, counting) more cleanly than manual dict logic.- Generator expressions are preferable to list comprehensions when you only need to iterate once and don't need the full collection in memory.
- Be cautious with mutable default arguments (
def f(x=[])) — they are created once, at function definition time, and shared across all calls.
Best Practices Checklist#
- Use lists for ordered, changeable collections
- Use tuples for fixed, related groups of values
- Use dictionaries for key-based lookups
- Use sets when uniqueness and fast membership testing matter
- Prefer comprehensions over manual loops for simple transformations
- Use generator expressions instead of list comprehensions for one-time iteration over large data
- Reach for
defaultdict/Counterinstead of manual dictionary bookkeeping - Use
.get()or.setdefault()instead of direct key access when a key might be missing - Avoid mutable default arguments in function definitions
- Use
copy.deepcopy()when nested mutable structures must be fully independent - Remember tuples (and frozensets) can be dictionary keys, but lists and sets cannot