No history yet

Pythonic Data Structures

Beyond Single Variables

You're comfortable with assigning single values to variables, like name = "Alice" or score = 95. But what happens when you need to manage a whole class of scores, a directory of names, or the complex attributes of a single user? Storing each piece of data in its own variable would be chaotic. Python provides powerful collection types—lists, tuples, dictionaries, and sets—to handle these structured-data challenges.

Think of them as different kinds of containers, each with its own strengths. A list is like a numbered rack of items that you can change at any time. A dictionary is like a file cabinet where each file has a unique label for quick retrieval. A tuple is a sealed, unchangeable list, and a set is an unordered bag of unique items. Choosing the right one is key to writing clean, efficient code.

The Right Tool for the Job

A crucial distinction between these containers is whether they are mutable or immutable types. Mutable objects, like lists and dictionaries, can be changed after they're created. You can add, remove, or change elements in a list without creating an entirely new list. Immutable objects, like tuples and strings, cannot be altered. Once you create a tuple, its contents are fixed for life. This might seem restrictive, but it provides safety and predictability, ensuring that the data doesn't change unexpectedly.

Another major consideration is lookup speed. When you need to check if an item exists in a collection, your choice of data structure has a huge impact. Checking for an item in a list requires Python to look at each element one by one until it finds a match. For a list with millions of items, this can be slow.

Dictionaries and sets, on the other hand, are optimized for this exact task. They use a technique called hashing to instantly locate an item, much like using an index in a book. This means that checking for membership in a set or dictionary takes roughly the same amount of time, regardless of whether it has ten items or ten million. This property is described in as O(1)O(1) (constant time) for sets/dictionaries versus O(n)O(n) (linear time) for lists.

Rule of thumb: If you need to frequently check if an item exists in a collection, use a set or a dictionary. If you need an ordered sequence that you can modify, use a list.

Efficient Sequences

For ordered data like lists and tuples, Python offers elegant ways to access and create subsets of your data. You already know basic indexing like my_list[0]. Advanced slicing lets you do more, using a [start:stop:step] syntax.

# A list of numbers
numbers = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

# Get every second element from the beginning
# Output: [0, 20, 40, 60, 80]
print(numbers[::2])

# Get the last three elements
# Output: [70, 80, 90]
print(numbers[-3:])

# Reverse the list completely
# Output: [90, 80, 70, 60, 50, 40, 30, 20, 10, 0]
print(numbers[::-1])

When you need to create a new list by transforming an existing one, a for loop works, but a list comprehension is often more readable and efficient. It's a compact syntax for creating a list based on an iterable.

# Using a for loop
squares = []
for i in range(10):
    squares.append(i * i)

# Using a list comprehension
squares_comp = [i * i for i in range(10)]

# You can even add conditions
even_squares = [i * i for i in range(10) if i % 2 == 0]

For very large datasets, a list comprehension can consume a lot of memory because it builds the entire list at once. In these cases, are a better choice. They look like list comprehensions but use parentheses instead of square brackets. A generator yields one item at a time, making it incredibly memory-efficient.

import sys

# A list comprehension (uses more memory)
list_comp = [i for i in range(10000)]
print(f"List comprehension size: {sys.getsizeof(list_comp)} bytes")

# A generator expression (uses very little memory)
gen_exp = (i for i in range(10000))
print(f"Generator expression size: {sys.getsizeof(gen_exp)} bytes")

Modeling Complex Data

The real power of these data structures comes from combining them. You can create lists of dictionaries, dictionaries of lists, or any other combination to model complex, real-world information. This is a common pattern in data processing, web APIs, and configuration files.

For example, a list of dictionaries is a perfect way to represent a collection of users, where each user has a set of named attributes.

users = [
    {
        "id": 101,
        "name": "Alice",
        "roles": ["admin", "editor"]
    },
    {
        "id": 102,
        "name": "Bob",
        "roles": ["viewer"]
    },
    {
        "id": 103,
        "name": "Charlie",
        "roles": ["editor", "commenter"]
    }
]

# Accessing nested data
# Get the first user's name
first_user_name = users[0]["name"] # "Alice"

# Get the second user's roles
bobs_roles = users[1]["roles"] # ["viewer"]

# Find all users with the 'editor' role using a list comprehension
editors = [user["name"] for user in users if "editor" in user["roles"]]
# editors is now ["Alice", "Charlie"]

By mastering these fundamental structures and the Pythonic ways to manipulate them, you can organize and process data with both clarity and efficiency.

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

Quiz Questions 1/6

Which of the following Python data structures is immutable, meaning its contents cannot be changed after creation?

Quiz Questions 2/6

You have a collection of one million unique product IDs. You need to write a function that frequently checks if a given ID exists in the collection. Which data structure would provide the fastest performance for this check?