No history yet

Idiomatic Sequences and Logic

Smarter Ways to Loop

You already know how to build a list of numbers using a for loop. It's a fundamental pattern in almost every programming language. But Python often provides a more elegant and efficient way to handle common tasks like transforming or filtering sequences. This is often called writing "Pythonic" code.

Let's say we want a list of the first ten perfect squares. The classic approach looks like this:

squares = []
for i in range(1, 11):
    squares.append(i * i)
# squares is [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

This works perfectly fine, but it takes four lines to express a simple idea. We can do better with a list comprehension—a concise, single-line expression for creating lists from other sequences.

squares = [i * i for i in range(1, 11)]
# squares is [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Notice how the structure mirrors the original loop. We have the expression (i * i), the loop itself (for i in range(1, 11)), and it's all wrapped in square brackets [] to signify we're building a list. This isn't just shorter; it's often faster because the iteration happens in optimized C code under the hood.

You can also embed logic directly inside. To get the squares of only the odd numbers, just add an if clause at the end:

odd_squares = [i * i for i in range(1, 11) if i % 2 != 0]
# odd_squares is [1, 9, 25, 49, 81]

This pattern extends to sets and dictionaries too. If you want a set of unique squared values from a list with duplicates, just swap the brackets for curly braces {}:

numbers = [1, 2, 2, 3, 3, 3]
unique_squares = {x * x for x in numbers}
# unique_squares is {1, 4, 9}

For dictionaries, you provide a key-value pair separated by a colon:

number_to_square = {x: x * x for x in range(1, 6)}
# number_to_square is {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Advanced Slicing

You're likely familiar with basic slicing to grab parts of a list, like my_list[0:5]. But slicing has a third argument: the step. The full syntax is sequence[start:stop:step].

This lets you pull out elements at regular intervals. For example, to get every other item in a list:

letters = ['a', 'b', 'c', 'd', 'e', 'f']
every_other = letters[::2]  # start defaults to 0, stop defaults to end
# every_other is ['a', 'c', 'e']

A negative step value moves backward through the sequence. A common and clever trick for reversing any sequence is to use a step of -1:

reversed_letters = letters[::-1]
# reversed_letters is ['f', 'e', 'd', 'c', 'b', 'a']

Under the hood, this bracket notation is syntactic sugar for a slice object. When you write letters[::2], Python is actually creating an object like slice(None, None, 2) and passing it to the list. You can create these objects yourself and reuse them, which can make your code cleaner if you have complex slicing logic that appears in multiple places.

data = list(range(20))

# Define a slice to get the second item from every group of 5
SECOND_IN_GROUP = slice(1, None, 5)

result = data[SECOND_IN_GROUP]
# result is [1, 6, 11, 16]

Truthiness and Short-Circuits

In Python, you don't always need an explicit comparison to True or False. Many objects have an inherent boolean value, a concept called truthinesss. For instance, empty sequences (lists, strings, dictionaries) are considered "falsy," while non-empty ones are "truthy."

ValueTruthiness
[], (), {}, "", set()Falsy
0, 0.0, None, FalseFalsy
Any non-empty sequenceTruthy
Any non-zero numberTruthy
TrueTruthy

This allows for concise checks. Instead of writing if len(my_list) > 0:, you can simply write if my_list:.

This concept pairs powerfully with short-circuiting in and and or operations. Python evaluates these expressions from left to right and stops as soon as it knows the final outcome.

For A or B, if A is truthy, Python returns A immediately without ever looking at B. For A and B, if A is falsy, Python returns A immediately without ever looking at B.

This isn't just a performance optimization; it's a tool for control flow. It's often used to provide default values:

# If user_name is an empty string (falsy), the expression evaluates to 'Guest'.
username = user_name or 'Guest'

It can also prevent errors. In the code below, person.name is only accessed if person is not None (truthy), avoiding a potential AttributeError.

if person and person.name == 'Alice':
    print('Found Alice!')

Finally, Python has a conditional expression, sometimes called a ternary operator, for writing a simple if/else statement on one line. It's great for simple assignments.

# Traditional if/else
if score > 60:
    result = 'pass'
else:
    result = 'fail'

# Conditional expression
result = 'pass' if score > 60 else 'fail'

Efficient Looping with Itertools

The standard library's itertools module is a treasure trove of functions for creating fast, memory-efficient iterators. It's like a toolkit for advanced looping. These functions operate lazily, meaning they generate one item at a time instead of building a whole new list in memory.

Let's look at a few common tools from itertools.

itertools.chain

other

Treats consecutive sequences as a single sequence, without creating a new combined list.

import itertools

list_a = [1, 2, 3]
list_b = ['a', 'b', 'c']

for item in itertools.chain(list_a, list_b):
    print(item)  # Prints 1, 2, 3, a, b, c

itertools.islice

other

Performs slicing on an iterator, returning items without creating a copy. It works just like standard slicing but on any iterable.

# Get elements from index 5 up to (but not including) 10
for i in itertools.islice(range(100), 5, 10):
    print(i) # Prints 5, 6, 7, 8, 9

itertools.product

other

Calculates the Cartesian product of input iterables. It's a memory-efficient way to create nested loops.

suits = ['♠', '♥', '♦', '♣']
ranks = ['A', 'K', 'Q', 'J']

# Instead of nested for loops
for suit, rank in itertools.product(suits, ranks):
    print(f'{rank}{suit}') # Prints A♠, K♠, Q♠, J♠, A♥, ...

Exploring itertools is a great next step in writing high-performance Python. These tools help you express complex iteration patterns clearly and with minimal memory overhead.

Quiz Questions 1/6

What is the primary advantage of using a list comprehension like [x*x for x in range(10)] over a traditional for loop?

Quiz Questions 2/6

Given the list letters = ['a', 'b', 'c', 'd', 'e', 'f'], what would the expression letters[::-2] produce?

Writing Pythonic code is about choosing the right tool for the job. Comprehensions, advanced slicing, and itertools help you write code that is not just functional, but also clear, concise, and efficient.