Skip to Content
Course content

44: Choosing the Right Collection Type

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing code from developers who are technically proficient but still fall into the "list trap." It happens because lists are the most intuitive collection in Python—they're just a sequence of things. When you aren't sure what to use, your brain defaults to a list. But as your data grows, using a list for the wrong job is one of the fastest ways to kill your application's performance.

Let's look at a real-world scenario: you're building a moderation system for a forum, and you have a collection of 10,000 banned user IDs. Every time a user tries to post a comment, you need to check if their ID is in that banned list.

The Cost of Scanning a List

# The naive approach
banned_users = [102, 405, 992, 1004, ...] # Imagine 10,000 IDs here

def can_post(user_id):
    if user_id in banned_users:
        return False
    return True

On the surface, this looks perfectly clean. But here is what's happening under the hood: the in operator for a list performs a linear search. Python starts at index 0 and asks, "Is this the ID?" then moves to index 1, then 2, and so on. If the user isn't banned, Python has to check every single one of those 10,000 elements before it can confidently tell you the user is allowed to post. I call this "walking the line." It's fine for ten items, but when you're doing this for every single request on a high-traffic site, you're wasting a massive amount of CPU cycles on a simple membership check.

Why the Set Changes Everything

If you don't care about the order of the IDs and you know they should be unique, a set is almost always the correct choice for membership tests. A set doesn't "walk the line"; it uses a hash table.

# The professional approach
banned_users = {102, 405, 992, 1004, ...} # A set literal

def can_post(user_id):
    if user_id in banned_users:
        return False
    return True

The syntax is nearly identical—just curly braces instead of square ones—but the performance difference is staggering. Looking up an item in a set takes constant time, $O(1)$, regardless of whether you have ten users or ten million. The computer calculates a hash of the ID and jumps directly to the memory location where that ID would be. You've effectively traded a tiny bit of memory for a massive increase in speed.

Mapping Values instead of Just Checking Existence

Now, what happens if you don't just need to know if a user is banned, but why they were banned? This is where you move from a set to a dictionary. I see people try to solve this by keeping two lists in parallel (one for IDs and one for reasons), which is a nightmare to maintain. If you sort one and forget the other, your data is corrupted.

A dictionary lets you associate the unique key (the user ID) with a value (the ban reason). Like sets, dictionaries use hash tables, so the lookup speed remains $O(1)$. You get the performance of a set with the added benefit of structured data.

# Mapping IDs to reasons
banned_users = {
    102: "Spamming",
    405: "Harassment",
    992: "Terms of Service violation"
}

def get_ban_status(user_id):
    # .get() prevents the program from crashing if the ID isn't found
    reason = banned_users.get(user_id)
    if reason:
        return f"Banned for: {reason}"
    return "Active"

Choosing Based on the Constraint

So, how do you decide in the moment? I usually ask myself three questions. First: Do I need to maintain the order of elements? If yes, stick with a list. Second: Do I need to ensure every element is unique? If yes, use a set. Third: Do I need to associate a piece of data with a unique identifier? Use a dictionary.

One last thing: don't forget about tuples. I use them when I want to signal to other developers (and my future self) that this collection should not change. If you have a set of coordinates for a map or a fixed configuration of server ports, a tuple is a safer bet than a list because it's immutable. It prevents accidental .append() calls from breaking your logic elsewhere in the app.




📋 Practical Task

Refactoring the Inventory Audit System

You've been handed a script that audits a warehouse inventory. The current implementation uses a list of tuples to store product IDs and their current stock levels. The script is running incredibly slowly because it iterates through the entire list every time it needs to update a stock count.

Your Task: Refactor the update_stock function. Instead of using a list of tuples, convert the inventory data into a more appropriate collection type that allows for fast lookups and updates by product ID.

# Current slow implementation
inventory = [
    ("PROD_001", 10),
    ("PROD_002", 5),
    ("PROD_003", 20),
    # ... imagine thousands of products
]

def update_stock(product_id, amount):
    for i in range(len(inventory)):
        if inventory[i][0] == product_id:
            # Update the stock value in the tuple (which is tricky since tuples are immutable)
            current_stock = inventory[i][1]
            inventory[i] = (product_id, current_stock + amount)
            return True
    return False

# Test the refactor
update_stock("PROD_002", 15)
print(inventory) 

Modify the data structure and the function so that you no longer need a for loop to find the product.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.