Pythonic Patterns¶
Idiomatic Python patterns for clean, efficient, and readable code.
Mental Model
Pythonic code follows one guiding principle: say what you mean, not how to do it. Prefer EAFP over LBYL, use built-in protocols (iteration, context management, unpacking) instead of manual loops and flags, and let duck typing handle polymorphism naturally.
EAFP vs LBYL¶
Python favors "Easier to Ask Forgiveness than Permission" (EAFP) over "Look Before You Leap" (LBYL).
EAFP Style (Pythonic)¶
```python
Try the operation, handle exceptions¶
try: value = dictionary[key] except KeyError: value = default
File operations¶
try: with open('file.txt') as f: data = f.read() except FileNotFoundError: data = "" ```
LBYL Style (Less Pythonic)¶
```python
Check before operating¶
if key in dictionary: value = dictionary[key] else: value = default
File check¶
if os.path.exists('file.txt'): with open('file.txt') as f: data = f.read() ```
When to Use Each¶
- EAFP: When the operation usually succeeds
- LBYL: When checking is cheaper than exception handling
Context Managers¶
Use with statements for resource management:
```python
File handling¶
with open('file.txt') as f: data = f.read()
File automatically closed¶
Multiple resources¶
with open('in.txt') as src, open('out.txt', 'w') as dst: dst.write(src.read())
Database connections¶
with connection.cursor() as cursor: cursor.execute(query)
Locks¶
with threading.Lock(): shared_resource.modify() ```
Comprehensions¶
List Comprehensions¶
```python
Pythonic¶
squares = [x**2 for x in range(10)] evens = [x for x in numbers if x % 2 == 0]
With condition¶
positive = [x for x in data if x > 0]
Nested¶
matrix = [[i*j for j in range(3)] for i in range(3)] ```
Dictionary Comprehensions¶
```python
Create dict from lists¶
pairs = {k: v for k, v in zip(keys, values)}
Transform dict¶
upper_dict = {k.upper(): v for k, v in original.items()}
Filter dict¶
filtered = {k: v for k, v in data.items() if v > 0} ```
Set Comprehensions¶
python
unique_lengths = {len(word) for word in words}
Generator Expressions¶
```python
Memory efficient for large data¶
total = sum(x**2 for x in range(1000000))
Lazy evaluation¶
gen = (expensive(x) for x in data) ```
Common Design Patterns¶
Factory Functions¶
```python def create_user(name, email, role='user'): """Factory function for user creation.""" return { 'name': name, 'email': email, 'role': role, 'created_at': datetime.now() }
Usage¶
admin = create_user('Alice', 'alice@example.com', role='admin') ```
Builder Pattern¶
```python class QueryBuilder: def init(self): self._table = None self._columns = ['*'] self._conditions = []
def table(self, name):
self._table = name
return self
def select(self, *columns):
self._columns = columns
return self
def where(self, condition):
self._conditions.append(condition)
return self
def build(self):
query = f"SELECT {', '.join(self._columns)} FROM {self._table}"
if self._conditions:
query += f" WHERE {' AND '.join(self._conditions)}"
return query
Fluent interface¶
query = (QueryBuilder() .table('users') .select('name', 'email') .where('active = 1') .build()) ```
Registry Pattern¶
```python _handlers = {}
def register(name): """Decorator to register handlers.""" def decorator(func): _handlers[name] = func return func return decorator
def get_handler(name): return _handlers.get(name)
Usage¶
@register('csv') def process_csv(data): return parse_csv(data)
@register('json') def process_json(data): return json.loads(data)
Dispatch¶
handler = get_handler(file_type) result = handler(data) ```
Assignment Patterns¶
Tuple Unpacking¶
```python
Multiple assignment¶
a, b = 1, 2 x, y, z = point
Extended unpacking¶
first, rest = [1, 2, 3, 4, 5] head, middle, tail = data
Ignore values¶
, important, _ = get_values() first, *, last = sequence ```
Swapping¶
```python
Pythonic swap (no temp variable)¶
a, b = b, a
Multiple swaps¶
a, b, c = c, a, b ```
Walrus Operator¶
```python
Assign and test in one expression¶
if (n := len(data)) > 10: print(f"Large dataset: {n} items")
In while loops¶
while (line := file.readline()): process(line)
In list comprehensions¶
results = [y for x in data if (y := transform(x)) is not None] ```
Default Values¶
```python
Or pattern for defaults¶
name = user_input or "Anonymous" config = options.get('config') or default_config
Conditional expression¶
result = value if condition else default ```
Summary¶
| Pattern | Use Case | Example |
|---|---|---|
| EAFP | Operations that usually succeed | try/except |
| Context managers | Resource management | with open() |
| Comprehensions | Transform/filter collections | [x for x in data] |
| Factory functions | Object creation | create_user() |
| Registry | Plugin systems | @register('name') |
| Unpacking | Multiple assignment | a, *rest = data |
| Walrus | Assign in expressions | if (n := len(x)): |
Key principles:
- Prefer EAFP for cleaner code
- Always use context managers for resources
- Use comprehensions for clarity and performance
- Choose patterns that fit your problem
Exercises¶
Exercise 1. EAFP vs LBYL have different behavior under race conditions. Predict which pattern is safer:
```python import os
filename = "data.txt"
LBYL¶
if os.path.exists(filename): with open(filename) as f: data = f.read()
EAFP¶
try: with open(filename) as f: data = f.read() except FileNotFoundError: data = "" ```
What can go wrong with the LBYL pattern between the exists() check and the open() call? Why is EAFP the preferred Python idiom?
Solution to Exercise 1
The EAFP pattern is safer. With LBYL, a race condition (TOCTOU -- Time of Check to Time of Use) can occur: the file could be deleted by another process between os.path.exists(filename) returning True and open(filename) executing. The LBYL code would then raise FileNotFoundError anyway, with no handler in place.
EAFP avoids this because the check and the operation are atomic from the application's perspective: you try to open the file, and if it fails, you handle the error. There is no window where the file's state can change between checking and using.
EAFP is also more Pythonic because:
- It handles the "usually succeeds" case with zero overhead (no preliminary check)
- It uses Python's exception mechanism, which is well-optimized for the no-exception path
- It covers all failure modes, not just the ones you thought to check for
Exercise 2.
The walrus operator (:=) assigns inside expressions. Predict the output:
```python data = [1, 2, 3, 4, 5, 6, 7, 8]
result = [y for x in data if (y := x ** 2) > 10] print(result)
What about this?¶
total = 0 running = [total := total + x for x in [1, 2, 3, 4]] print(running) print(total) ```
Why does total have the final value outside the comprehension? How does the walrus operator's scoping differ from regular comprehension variables?
Solution to Exercise 2
Output:
text
[16, 25, 36, 49, 64]
[1, 3, 6, 10]
10
The first comprehension filters to keep only squares greater than 10: 1, 4, 9 are excluded; 16, 25, 36, 49, 64 are kept.
total has value 10 outside the comprehension because the walrus operator (:=) assigns to the enclosing scope, not the comprehension's internal scope. Regular comprehension variables (like x) are scoped to the comprehension and do not leak. But := explicitly targets the enclosing scope, so total accumulates across iterations and persists after the comprehension.
This scoping behavior is by design (PEP 572): the walrus operator always binds in the enclosing scope, making it useful for accumulation patterns but requiring care to avoid surprising side effects.
Exercise 3.
Tuple unpacking with * is more powerful than it appears. Predict the output:
```python first, *middle, last = [1, 2, 3, 4, 5] print(first, middle, last)
a, *b = [1] print(a, b)
*c, d = [1] print(c, d)
head, * = "hello" print(head) print(type()) ```
Why does *b become an empty list [] when unpacking [1]? What type does the starred variable always produce?
Solution to Exercise 3
Output:
text
1 [2, 3, 4] 5
1 []
[] 1
h
<class 'list'>
*b in a, *b = [1] becomes [] because after assigning a = 1, there are no remaining elements. The starred variable always produces a list, even if it captures zero elements. This is a design guarantee: you can always iterate over the starred result without checking if it is None or a single value.
The starred variable absorbs whatever is left after the non-starred variables claim their positions. first and last each claim one element from the ends; *middle gets everything between them.
_ is conventionally used for values you want to discard, but it is still a regular variable. In this case, *_ is a list of discarded characters: ['e', 'l', 'l', 'o'].