Mastering Practical Python Development
Advanced Data Structures
Smarter Ways to Organise Data
You already know how to store data in lists and dictionaries. These are the workhorses of Python, but sometimes you need a more specialised tool. Choosing the right data structure can make your code faster, more readable, and less prone to bugs. Let's look at a few powerful options that go beyond the basics.
Efficient Sequences
Building a new list from an existing one is a common task. You've probably written for loops to do this. For example, to get a list of squared numbers:
# The traditional way
squares = []
for i in range(10):
squares.append(i * i)
# squares is now [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
List comprehensions do the same thing in a single, more expressive line. They're often faster to execute and easier to read once you're used to the syntax.
# Using a list comprehension
squares = [i * i for i in range(10)]
# You can also add conditions
even_squares = [i * i for i in range(10) if i % 2 == 0]
# even_squares is [0, 4, 16, 36, 64]
What if you're working with a massive sequence and don't want to hold it all in memory at once? That's where come in. They look like list comprehensions but use parentheses instead of square brackets. Instead of building a full list, they create an iterator that yields one item at a time, saving memory.
# This doesn't store 1,000,000 numbers in memory
squares_generator = (i * i for i in range(1000000))
# You can iterate over it like any other sequence
for num in squares_generator:
if num > 100:
print(num)
break # We stop after finding the first one
# Output: 121
Uniqueness and Fast Lookups
Imagine you have a list of user IDs and need to quickly check if a specific user has access, or you want a list of unique tags from a blog post. A list would work, but it would be slow. For these tasks, a set is the ideal choice.
A set is an unordered collection of unique items. Its main advantage is incredibly fast membership testing. Checking if an item is in a set is significantly quicker than checking if it's in a list, especially for large collections. This efficiency comes down to its underlying implementation, which impacts its ..
tags = ['python', 'data', 'code', 'python']
unique_tags = set(tags)
# unique_tags is now {'code', 'data', 'python'}
# Fast membership testing
print('python' in unique_tags) # True
print('java' in unique_tags) # False
Sets also support mathematical operations. You can find the union (all items from both sets), intersection (items in both sets), and difference (items in one set but not the other) using simple operators.
| Operation | Operator | Example | Result |
|---|---|---|---|
| Union | ` | ` | `set_a |
| Intersection | & | set_a & set_b | Items that appear in both sets |
| Difference | - | set_a - set_b | Items in set_a but not in set_b |
| Symmetric Diff | ^ | set_a ^ set_b | Items in one set, but not both |
The Collections Toolbox
Python's standard library includes the collections module, which offers even more specialised data structures. Think of it as a workshop full of precision tools for specific jobs.
namedtuple
other
Creates tuple subclasses with named fields. This lets you access elements by name instead of just by index, making your code more self-documenting.
from collections import namedtuple
# Define the structure
Point = namedtuple('Point', ['x', 'y'])
# Create an instance
p1 = Point(10, 20)
# Access by name or index
print(p1.x) # 10
print(p1[1]) # 20
Another useful tool is deque, which stands for "double-ended queue." It's like a list but is highly optimised for adding and removing elements from both the front and the back. Regular lists are fast at appending to the end (append), but removing from the beginning (pop(0)) is slow because all other elements have to be shifted over.
A solves this problem, making it perfect for implementing queues or keeping a running history of the last few items seen.
from collections import deque
# Create a deque with a max length of 3
last_three_items = deque(maxlen=3)
last_three_items.append('A')
last_three_items.append('B')
last_three_items.append('C')
print(last_three_items) # deque(['A', 'B', 'C'], maxlen=3)
# Adding a new item pushes the oldest one out
last_three_items.append('D')
print(last_three_items) # deque(['B', 'C', 'D'], maxlen=3)
Finally, the Counter object is a specialised dictionary for counting hashable objects. It's a simple way to tally up items in a list.
from collections import Counter
votes = ['red', 'blue', 'red', 'green', 'blue', 'red']
vote_count = Counter(votes)
print(vote_count)
# Counter({'red': 3, 'blue': 2, 'green': 1})
# It also has useful methods
print(vote_count.most_common(1))
# [('red', 3)]
Now let's test your understanding of these advanced data structures.
What is the primary advantage of using a generator expression (i for i in range(10)) over a list comprehension [i for i in range(10)]?
You need to store a large collection of unique user IDs and frequently check if a specific ID is present. Which data structure offers the best performance for this membership testing?
Selecting the right data structure is a key step towards writing professional, efficient Python code. By looking beyond lists and dictionaries, you can solve problems more elegantly and build applications that perform better at scale.