No history yet

Intermediate Data Structures

Beyond Simple Lists

You're already familiar with lists for storing items in sequence. They're great for ordered collections, but what if you need to find an item quickly without looping through the entire collection? Searching a long list is like flipping through a novel page by page to find a specific word. It works, but it's slow.

This is where dictionaries come in. Dictionaries store data not as an ordered sequence, but as a collection of key-value pairs. Think of it like a real-world dictionary: you look up a word (the key) to find its definition (the value). This lookup is nearly instantaneous, no matter how many items are in the dictionary.

Data StructureLookup Time (Average Case)
ListO(n)O(n) - Proportional to size
DictionaryO(1)O(1) - Constant time

This difference in time complexity is crucial. As your data grows, list lookups get progressively slower, while dictionary lookups remain fast. Let's see how to create one.

# A dictionary storing configuration for a web app
config = {
    "api_key": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
    "timeout": 30,
    "retries": 3,
    "debug_mode": False
}

# Accessing a value is simple and fast
api_key = config["api_key"]
print(f"API Key: {api_key}")

# Adding a new key-value pair
config["version"] = "2.1"

# Modifying an existing value
config["timeout"] = 60

Dictionaries also provide special "view objects" that give you a dynamic look into the dictionary's keys, values, or key-value pairs. These views update automatically if the dictionary changes.

print("Keys:", config.keys())
# Output: Keys: dict_keys(['api_key', 'timeout', 'retries', 'debug_mode', 'version'])

print("Values:", config.values())
# Output: Values: dict_values(['a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8', 60, 3, False, '2.1'])

print("Items:", config.items())
# Output: Items: dict_items([('api_key', 'a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8'), ('timeout', 60), ...])

Storing Complex Data

Real-world data is rarely flat. You often need to store structured, hierarchical information. Dictionaries can contain other dictionaries, creating nested structures that are perfect for this.

app_config = {
    'database': {
        'host': 'localhost',
        'port': 5432,
        'user': 'admin',
        'password': 'secure_password_123'
    },
    'api_settings': {
        'version': 'v2',
        'endpoints': ['/users', '/products']
    },
    'feature_flags': {
        'new_dashboard': True,
        'beta_access': False
    }
}

# Accessing nested data
db_host = app_config['database']['host']
print(f"Database host: {db_host}")

# Toggling a feature flag
app_config['feature_flags']['beta_access'] = True

This approach is much cleaner than trying to manage complex data with lists alone. It keeps related information organized and easy to access.

Working with Unique Items

Another common task is managing collections where you only care about unique items. For example, tracking the unique IP addresses that visited your website. You could use a list and check for duplicates before adding a new IP, but that's inefficient.

A better tool for this job is the set. Sets are unordered collections of unique elements. They automatically handle deduplication for you.

visitor_ips = ['192.168.1.1', '10.0.0.5', '192.168.1.1', '172.16.0.10', '10.0.0.5']

# Convert the list to a set to get unique IPs
unique_visitors = set(visitor_ips)

print(unique_visitors)
# Output: {'172.16.0.10', '10.0.0.5', '192.168.1.1'}

Sets are powerful because they support mathematical operations like union, intersection, and difference. These are invaluable for comparing collections of data.

# Let's say we have two sets of user IDs
premium_users = {101, 204, 305, 401}
active_today = {99, 101, 204, 500}

# Intersection: Users who are premium AND active today
active_premium_users = premium_users.intersection(active_today)
print(f"Active premium users: {active_premium_users}")
# Output: Active premium users: {101, 204}

# Union: All users who are either premium OR active today (or both)
all_users_today = premium_users.union(active_today)
print(f"All unique users today: {all_users_today}")
# Output: All unique users today: {99, 101, 401, 204, 500, 305}

# Difference: Premium users who were NOT active today
inactive_premium_users = premium_users.difference(active_today)
print(f"Inactive premium users: {inactive_premium_users}")
# Output: Inactive premium users: {401, 305}

Sometimes you need an immutable version of a set, one that cannot be changed after it's created. This is what frozenset is for. Because they are immutable and hashable, frozensets can be used as keys in a dictionary, whereas regular sets cannot.

# A dictionary mapping a pair of ingredients (as a frozenset) to a recipe
recipe_lookup = {
    frozenset(['flour', 'eggs']): 'Pancakes',
    frozenset(['tomato', 'basil', 'mozzarella']): 'Caprese Salad'
}

my_ingredients = frozenset(['eggs', 'flour'])

print(f"With these ingredients, you can make: {recipe_lookup[my_ingredients]}")
# Output: With these ingredients, you can make: Pancakes

Let's check your understanding of these powerful data structures.

Quiz Questions 1/5

What is the primary advantage of using a dictionary over a list when you need to frequently search for specific items?

Quiz Questions 2/5

What will be the output of the following Python code snippet?

my_list = ['apple', 'banana', 'apple', 'orange', 'banana']
my_set = set(my_list)
print(len(my_set))

Choosing the right data structure is a key step in writing efficient, readable code. Dictionaries and sets provide powerful alternatives to lists when you need fast lookups, unique elements, or complex data storage.