No history yet

Pythonic Data Structures

Writing Pythonic Collections

You already know how to build lists and dictionaries. Now, let's learn to build them like a seasoned Python developer. The key is to use comprehensions, a concise way to create collections from existing iterables.

Instead of initializing an empty list and using a for loop to append elements, a list comprehension does it all in one line. It's not just shorter, it's often faster and more readable once you get the hang of it.

# The old way
squares = []
for i in range(10):
    if i % 2 == 0: # Only even numbers
        squares.append(i**2)

# The Pythonic way (list comprehension)
squares_comp = [i**2 for i in range(10) if i % 2 == 0]

print(squares)        # Output: [0, 4, 16, 36, 64]
print(squares_comp) # Output: [0, 4, 16, 36, 64]

The same syntax works for sets and dictionaries. For a set, just swap the square brackets for curly braces. For a dictionary, specify the key and value separated by a colon.

# Set comprehension (unique squared even numbers)
unique_squares = {i**2 for i in range(10) if i % 2 == 0}
# Output: {0, 4, 64, 16, 36}

# Dictionary comprehension (number -> its square)
square_map = {i: i**2 for i in range(5)}
# Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

List comprehensions create the entire list in memory at once. This is fine for small collections, but for very large datasets, it can consume a lot of RAM. The solution is a generator expressions. By swapping the square brackets for parentheses, you create an iterator that generates values on the fly, one by one, only when needed. This approach is far more memory-efficient for large-scale data processing.

Specialized Collections

Python's standard library includes the collections module, which offers specialized data structures for specific problems. Think of them as high-performance tools that can make your code cleaner and more efficient.

Let's say you're working with coordinate points. You could use a simple tuple like (10, 20). But which value is x and which is y? You have to remember the order. A namedtuple solves this by giving names to each position, combining the readability of an object with the lightweight nature of a tuple.

from collections import namedtuple

# Define the structure
Point = namedtuple('Point', ['x', 'y'])

# Create an instance
p1 = Point(10, 20)

# Access by name, just like an object
print(f"The x-coordinate is {p1.x}") # Output: The x-coordinate is 10

# Still works like a regular tuple
print(f"The y-coordinate is {p1[1]}") # Output: The y-coordinate is 20

Another common headache is checking if a key exists in a dictionary before adding to its value. For example, when counting word frequencies, you often write code to handle the first time you see a word. A defaultdict simplifies this by providing a default value for keys that don't exist yet.

from collections import defaultdict

# Create a defaultdict that returns 0 for missing keys
word_counts = defaultdict(int)

sentence = "the quick brown fox jumps over the lazy dog"
for word in sentence.split():
    word_counts[word] += 1 # No need to check if key exists!

print(word_counts['the']) # Output: 2
print(word_counts['cat']) # Output: 0 (it was never there, but returns default)

Slicing and Dicing

Slicing is a powerful way to extract subsequences from lists, tuples, and strings. You're likely familiar with the basic [start:stop] syntax, but the full version is [start:stop:step]. The step argument lets you skip items or even reverse the sequence.

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Get every second element
evens = numbers[0:10:2]
print(evens) # Output: [0, 2, 4, 6, 8]

# Reverse the list with a step of -1
reversed_numbers = numbers[::-1]
print(reversed_numbers) # Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# You can omit start and stop
# Get every third element starting from index 1
subset = numbers[1::3]
print(subset) # Output: [1, 4, 7]

A key detail: slicing a list creates a shallow copy. The new list is a distinct object, but the elements inside it are references to the same objects in the original list.

Structure and Performance

Choosing the right data structure involves trade-offs, primarily between memory usage, speed of access, and mutability. There is no single "best" data structure; the right choice depends entirely on your goal.

StructureMutabilityMemory UsageKey Use Case
listMutableHighOrdered collection of items that may change.
tupleImmutableLowOrdered, fixed collection of items (e.g., coordinates).
setMutableMediumStoring unique, unordered items for fast membership tests.
dictMutableHighStoring key-value pairs for fast lookups by key.
namedtupleImmutableVery LowA tuple where element positions have readable names.

For example, a tuple is more memory-efficient than a list because it's immutable. Python doesn't need to allocate extra space in case you decide to add more elements. If your data doesn't need to change, a tuple is often a better choice. Similarly, if you need to quickly check if an item exists in a large collection, a set is much faster than a list because its items are hashed for near-instant lookups.

Time to test your knowledge on these Pythonic techniques.

Quiz Questions 1/5

What is the primary advantage of using a generator expression like (i for i in range(1000000)) over a list comprehension [i for i in range(1000000)]?

Quiz Questions 2/5

Which line of code correctly creates a dictionary mapping each number from 0 to 4 to its square?

Mastering these structures and techniques will make your code more efficient, readable, and professional.