Skip to main content
Python beginner Lesson 10 of 28

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 caseBest type
Ordered, changeable collectionlist
Fixed record (coordinates, config row)tuple
Fast key-value lookupdict
Membership tests, deduplicationset
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.

Frequently Asked Questions

When should I use a tuple instead of a list?
Use tuples for data that should not change (immutable records), such as coordinates, RGB colours, or database rows. They are also slightly faster than lists and can be used as dictionary keys.
What is the time complexity of dictionary lookups?
Dictionary (dict) lookups in Python are O(1) on average because they are backed by a hash table. This makes them ideal for fast key-value access.
What is the difference between a set and a frozenset?
A set is mutable — you can add and remove elements. A frozenset is immutable and hashable, so it can be used as a dictionary key or stored inside another set.