Intermediate Python Programming Foundations
Advanced Data Structures
Smarter Ways to Build Collections
You're likely familiar with building lists and dictionaries using loops. It's a fundamental pattern: initialize an empty collection, loop through some data, and append items one by one. This works, but Python offers a more concise and often faster way to do the same thing: comprehensions.
Imagine you want a list of the first ten square numbers. Using a loop, you'd write something like this:
squares = []
for i in range(10):
squares.append(i * i)
# squares is now [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
A list comprehension packs all of that logic into a single, readable line.
squares = [i * i for i in range(10)]
# same result, less code
The structure is [expression for item in iterable]. You can even add a condition. Let's say we only want the squares of even numbers:
even_squares = [i * i for i in range(10) if i % 2 == 0]
# even_squares is [0, 4, 16, 36, 64]
This same principle applies to dictionaries, which is perfect for creating a dictionary from two lists or transforming an existing one.
# Create a dictionary of numbers and their squares
square_dict = {x: x*x for x in range(5)}
# square_dict is {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Sets for Uniqueness and Speed
What if you need to store a collection of items, but you only want to keep unique values? You could write code to check if an item is already in a list before adding it, but there's a data structure built just for this: the set.
A set automatically enforces uniqueness. If you try to add an item that's already present, nothing happens. No error, no duplication.
colors = ['red', 'green', 'blue', 'red', 'yellow', 'blue']
unique_colors = set(colors)
# unique_colors is {'green', 'blue', 'yellow', 'red'}
# Note: sets are unordered, so the sequence may vary
unique_colors.add('green') # This does nothing, 'green' is already in the set
print(unique_colors)
The real superpower of sets is speed. Checking if an item is in a set is incredibly fast, regardless of how many items the set contains. This is because sets are implemented using a , which allows for near-instant lookups. Checking for an item in a long list, on the other hand, requires Python to look at each element one by one until it finds a match. For a list with millions of items, that's a huge difference.
Use a list when the order of items matters and duplicates are okay. Use a set when you need to store unique items and perform fast membership checks.
Tuples and the Power of Immutability
At first glance, a tuple looks like a list that uses parentheses instead of square brackets. The critical difference isn't the syntax, but the behavior: tuples are immutable. Once you create a tuple, you cannot change, add, or remove its elements.
# A tuple of RGB color values
red = (255, 0, 0)
# This will raise a TypeError, because tuples are immutable
# red[0] = 240
Why would you want a data structure you can't change? serves a few key purposes. First, it acts as a signal to anyone reading your code that this data is not meant to be modified. It's a constant. Second, because they can't change, tuples can be used as keys in a dictionary, whereas lists cannot. This is useful for composite keys, like using (latitude, longitude) coordinates to store data about a location.
Specialty Tools from the Collections Module
Python's collections module is a treasure chest of specialized container types that solve common problems elegantly. Let's look at two of the most useful: defaultdict and namedtuple.
A defaultdict is a subclass of the standard dictionary. It fixes a common annoyance: trying to access or modify a key that doesn't exist. Normally, this would raise a KeyError.
# Standard dict - this raises a KeyError
word_counts = {}
word_counts['hello'] += 1
With a defaultdict, you provide a function (like int, list, or set) that will be used to create a default value for any key that doesn't exist. For counting, int works perfectly because it returns 0.
from collections import defaultdict
word_counts = defaultdict(int) # Use int() to provide a default value of 0
word_counts['hello'] += 1
# word_counts is now defaultdict(<class 'int'>, {'hello': 1})
Next up is namedtuple. It gives you the immutability of a tuple but with the ability to access elements by name instead of just by index. This makes your code much more readable.
from collections import namedtuple
# Define a 'Point' namedtuple
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(10, 20)
# Access by name, which is more descriptive
print(p1.x)
# You can still access by index
print(p1[1])
namedtupleprovides a lightweight way to create simple classes for grouping data together without the full overhead of defining a class yourself.
Knowing when to use a list, set, tuple, or a specialized container from the collections module is a hallmark of an effective Python programmer. By choosing the right data structure for the job, you can write code that is not only more efficient but also clearer and easier to maintain.
Time to test what you've learned.
Which of the following code snippets correctly creates a dictionary mapping numbers from 0 to 4 to their respective squares?
What is the primary advantage of using a set instead of a list when you need to frequently check if an item exists in a large collection?