Mastering Professional Python Patterns
Idiomatic Logic Mastery
Writing Python, Pythonically
You already know how to write Python that works. Now, let's focus on writing Python that sings. 'Pythonic' code isn't just about following rules; it's about embracing the language's philosophy of readability and simplicity. It’s the difference between giving directions with a long list of turns versus pointing to a landmark and saying, "Head for that."
One of the most powerful 'landmarks' in Python is the comprehension. You've likely written loops to build lists. It works, but it can be verbose.
# The old way
squares = []
for i in range(10):
if i % 2 == 0: # only even numbers
squares.append(i * i)
print(squares)
# [0, 4, 16, 36, 64]
A list comprehension does the same thing in a single, expressive line.
# The Pythonic way
squares = [i * i for i in range(10) if i % 2 == 0]
print(squares)
# [0, 4, 16, 36, 64]
Notice the structure: [expression for item in iterable if condition]. The condition is optional, but powerful. This isn't just about saving lines of code. It's about declaring what you want the new list to contain, rather than spelling out the step-by-step process of how to build it.
This pattern extends to sets and dictionaries, too. If you need a set of unique squared numbers or a dictionary mapping numbers to their squares, the syntax is intuitive and consistent.
| Type | Traditional Loop | Comprehension |
|---|---|---|
| List | squares = []for x in n: squares.append(x**2) | squares = [x**2 for x in n] |
| Set | squares = set()for x in n: squares.add(x**2) | squares = {x**2 for x in n} |
| Dict | squares = {}for x in n: squares[x] = x**2 | squares = {x: x**2 for x in n} |
Memory and Iteration
Comprehensions are great, but they have one drawback: they create the entire data structure in memory at once. For a list of a thousand numbers, that's fine. For a billion? Your computer might not be so happy.
This is where generators come in. A generator produces items one at a time, right when you need them. It doesn't store the whole sequence. You create a generator expression using parentheses instead of square brackets.
# A list comprehension (uses more memory)
list_comp = [i * i for i in range(1_000_000)]
# A generator expression (memory-efficient)
gen_exp = (i * i for i in range(1_000_000))
# Nothing has been calculated yet for the generator!
print(gen_exp) # <generator object <genexpr> at ...>
# The values are produced as you iterate
print(next(gen_exp)) # 0
print(next(gen_exp)) # 1
You can also create more complex generators using functions with the yield keyword. When a function contains yield, it automatically becomes a generator function. Each time you call next() on its result, the function runs until it hits yield, sends that value back, and then pauses its state, waiting for the next call.
This behavior is part of Python's , a fundamental design pattern. An object is an iterator if it implements two methods: __iter__() (which returns the object itself) and __next__(), which returns the next value and raises a StopIteration exception when it's done. for loops, comprehensions, and generators all use this protocol under the hood.
Smarter Unpacking and Logic
Python's expressiveness shines in how it handles variables and boolean logic. You're familiar with basic variable unpacking, like x, y = (1, 2). But what if you have more values than variables? Extended iterable unpacking lets you grab the items you want and collect the rest.
Using an asterisk (*) on a variable name assigns it a list of all items from the iterable that are left over after the other assignments have been made.
numbers = [1, 2, 3, 4, 5, 6]
first, *middle, last = numbers
print(f"First: {first}") # First: 1
print(f"Middle: {middle}") # Middle: [2, 3, 4, 5]
print(f"Last: {last}") # Last: 6
This is incredibly useful for processing data where you might only care about the head or tail of a sequence.
Finally, let's talk about "". In Python, you don't always need to make explicit comparisons in an if statement. Many objects have an inherent boolean value. Any non-empty collection (list, string, dict), any non-zero number, and the True object itself are considered "truthy." Empty collections, the number 0, None, and False are "falsy."
Instead of
if len(my_list) != 0:, you can simply writeif my_list:. Instead ofif user_name is not None and user_name != '':, you can writeif user_name:.
Relying on truthiness makes your conditional logic cleaner and more readable. It's a small change that signals a deeper understanding of the language's conventions.
Time to check your understanding of these more advanced concepts.
What is the output of the following Python code?
numbers = [1, 2, 3, 4, 5, 6]
squares_of_evens = [n**2 for n in numbers if n % 2 == 0]
print(squares_of_evens)
What is the primary advantage of using a generator expression like (x*x for x in range(1000000)) over a list comprehension like [x*x for x in range(1000000)]?