Table of Contents
- List Comprehensions: Concise Iteration
- Unpacking: Clean Variable Assignment
- Context Managers: Safe Resource Handling
- Leverage the
collectionsModule - Efficient String Formatting with F-Strings
- Error Handling with
elseandfinally - Generators: Memory-Efficient Iteration
- The Walrus Operator (:=): Assignment Expressions
- Decorators: Reusable Code Logic
- Profiling with
cProfileandtimeit
1. List Comprehensions: Concise Iteration
Python’s list comprehensions let you create lists in a single line, replacing clunky for loops with readable, efficient code. They’re faster than traditional loops and reduce boilerplate.
Why Use Them?
- Brevity: Condense 3–4 lines of loop code into one.
- Readability: Clearly expresses the intent (e.g., “filter even numbers” or “square all elements”).
- Performance: Slightly faster than manual loops due to optimized internal execution.
Example: Squaring Numbers
Traditional Loop:
squares = []
for i in range(10):
squares.append(i **2)
print(squares) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
```** List Comprehension:**```python
squares = [i** 2 for i in range(10)]
print(squares) # Same output
Example: Filtering with Conditions
Create a list of even numbers between 1 and 20:
evens = [x for x in range(1, 21) if x % 2 == 0]
print(evens) # Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Pro Tip: Use dictionary and set comprehensions too!
# Dictionary comprehension: {key: value for ...}
square_dict = {i: i **2 for i in range(5)} # {0:0, 1:1, 2:4, 3:9, 4:16}
# Set comprehension: {value for ...}
unique_squares = {i** 2 for i in [-2, -1, 0, 1, 2]} # {0, 1, 4}
2. Unpacking: Clean Variable Assignment
Unpacking lets you assign elements of iterables (lists, tuples, etc.) to variables in one line. It’s perfect for swapping values, splitting data, or handling function returns.
Basic Unpacking
Assign elements of a tuple/list to variables:
coordinates = (10, 20, 30)
x, y, z = coordinates # x=10, y=20, z=30
Swapping Variables Without a Temp
No need for temp = a; a = b; b = temp!
a, b = 5, 10
a, b = b, a # a=10, b=5 (swapped!)
Extended Unpacking with *
Use * to capture remaining elements into a list (Python 3+):
numbers = [1, 2, 3, 4, 5]
first, *middle, last = numbers # first=1, middle=[2,3,4], last=5
Use Case: Unpacking function arguments.
def greet(name, age):
print(f"Hello {name}, you're {age}!")
person = ("Alice", 30)
greet(*person) # Equivalent to greet("Alice", 30)
3. Context Managers: Safe Resource Handling
Context managers (via the with statement) automate resource cleanup (e.g., closing files, releasing network connections). They ensure resources are freed even if errors occur, avoiding leaks.
Why Use with?
- Guaranteed Cleanup: No need to manually call
close()—withhandles it. - Readability: Clearly scopes where the resource is used.
Example: Reading a File
Unsafe (Manual Cleanup):
file = open("data.txt", "r")
data = file.read()
file.close() # Easy to forget! Risk of unclosed files.
Safe (Context Manager):
with open("data.txt", "r") as file:
data = file.read() # File auto-closes when block ends
Custom Context Managers
Use contextlib.contextmanager to create your own. For example, a timer:
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
yield # Code inside `with` runs here
end = time.time()
print(f"Elapsed: {end - start:.2f}s")
with timer():
# Simulate work
time.sleep(1) # Output: Elapsed: 1.00s
4. Leverage the collections Module
The standard collections module provides specialized data structures to solve common problems more cleanly than built-in types (lists, dicts).
Key Tools:
-
defaultdict: AvoidsKeyErrorby auto-initializing missing keys.from collections import defaultdict counts = defaultdict(int) # Missing keys default to 0 counts["apple"] += 1 # No error! counts["apple"] = 1 -
Counter: Counts hashable objects (e.g., word frequencies).from collections import Counter words = ["apple", "banana", "apple", "orange", "apple"] word_counts = Counter(words) print(word_counts) # Counter({'apple': 3, 'banana': 1, 'orange': 1}) print(word_counts.most_common(2)) # [('apple', 3), ('banana', 1)] -
deque: Double-ended queue for O(1) appends/pops from both ends (faster than lists for this).from collections import deque dq = deque([1, 2, 3]) dq.append(4) # deque([1,2,3,4]) dq.popleft() # deque([2,3,4]) (O(1) time vs. O(n) for list.pop(0)) -
namedtuple: Creates readable tuples with named fields (like a lightweight class).from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(10, 20) print(p.x) # 10 (more readable than p[0])
5. Efficient String Formatting with F-Strings
Python offers multiple string formatting methods, but f-strings (Python 3.6+) are the fastest, most readable, and expressive.
Comparison of Methods
| Method | Syntax Example | Readability | Speed |
|---|---|---|---|
| %-formatting | "Hello %s, age %d" % (name, age) | Poor | Slow |
str.format() | "Hello {}, age {}".format(name, age) | Better | Faster |
| F-strings | f"Hello {name}, age {age}" | Best | Fastest |
F-String Superpowers
-
Inline Expressions: Compute values directly inside
{}.x = 5 print(f"5 squared is {x** 2}") # Output: 5 squared is 25 -
Format Specifiers: Control padding, rounding, etc.
pi = 3.14159 print(f"Pi: {pi:.2f}") # Rounds to 2 decimals: Pi: 3.14 -
Calling Functions:
def get_name(): return "Bob" print(f"Hello {get_name().upper()}!") # Output: Hello BOB!
6. Error Handling with else and finally
Python’s try-except blocks support else (runs if no exception) and finally (runs always), making error handling more granular.
Structure
try:
# Code that might fail
result = 10 / 2
except ZeroDivisionError:
# Runs if exception occurs
print("Cannot divide by zero!")
else:
# Runs ONLY if no exception
print(f"Result: {result}") # Output: Result: 5
finally:
# Runs ALWAYS (cleanup code)
print("Done!") # Output: Done!
Use Case: Validating Input
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "Error: Division by zero"
else:
print("Division succeeded!") # Only if no error
finally:
print("Division attempt complete.")
print(safe_divide(10, 2)) # Output: Division succeeded! / Division attempt complete. / 5.0
print(safe_divide(10, 0)) # Output: Division attempt complete. / Error: Division by zero
7. Generators for Memory-Efficient Iteration
Generators are iterators that yield values one at a time, instead of storing the entire sequence in memory. They’re ideal for large datasets (e.g., processing 1M+ rows).
How They Work
Use yield instead of return in a function. Each call to next() resumes execution from where it left off.
Example: Generating Fibonacci Numbers
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a # Yields one value at a time
a, b = b, a + b
# Use the generator
fib = fibonacci(5)
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1 (and so on)
Memory Benefit: A generator for 1M numbers uses ~KBs, while a list uses ~MBs.
Generator Expressions: Like list comprehensions but for generators (use () instead of []).
squares = (x** 2 for x in range(1000000)) # No memory bloat!
8. The Walrus Operator (:=): Assignment Expressions
Introduced in Python 3.8, the walrus operator (:=) lets you assign a value to a variable and use it in an expression. It’s great for reducing redundant code.
Use Cases
-
Simplifying
ifConditions:# Without walrus: Redundant len() call my_list = [1, 2, 3] if len(my_list) > 0: print(f"List has {len(my_list)} elements") # With walrus: Assign and check in one line if (n := len(my_list)) > 0: print(f"List has {n} elements") # Output: List has 3 elements -
Loop Conditions:
# Read lines until EOF while (line := input("Enter text: ")) != "quit": print(f"You entered: {line}")
9. Decorators: Reusable Code Logic
Decorators modify or enhance function behavior without changing the function itself. They’re perfect for logging, timing, authentication, and more.
How to Create a Decorator
A decorator is a function that wraps another function. Use functools.wraps to preserve the original function’s metadata (name, docstring).
Example: Timing a Function
import time
from functools import wraps
def timer_decorator(func):
@wraps(func) # Preserve func's metadata
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # Call the original function
end = time.time()
print(f"{func.__name__} took {end - start:.2f}s")
return result
return wrapper
# Use the decorator
@timer_decorator
def slow_function():
time.sleep(1)
slow_function() # Output: slow_function took 1.00s
Common Decorators: @classmethod, @staticmethod, @property (for OOP), and libraries like click (CLI tools).
10. Profiling with cProfile and timeit
To optimize code, first identify bottlenecks. Use cProfile (for detailed execution stats) and timeit (for timing small snippets).
cProfile: Profile Full Scripts
Run from the command line to see which functions take the most time:
python -m cProfile -s cumulative my_script.py
Output Example:
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 1.001 1.001 my_script.py:1(slow_function)
timeit: Time Small Code Snippets
Compare performance of alternatives (e.g., list comprehensions vs. loops):
import timeit
# Time list comprehension vs. loop
loop_time = timeit.timeit(
'squares = []; for i in range(1000): squares.append(i**2)',
number=10000
)
comprehension_time = timeit.timeit(
'squares = [i**2 for i in range(1000)]',
number=10000
)
print(f"Loop: {loop_time:.2f}s | Comprehension: {comprehension_time:.2f}s")
# Output: Loop: 1.23s | Comprehension: 0.89s (Faster!)
Conclusion
Python’s flexibility and depth make it a joy to use, but mastering these tips will help you write code that’s cleaner, faster, and more maintainable. From list comprehensions to decorators, each trick solves a common problem and unlocks new possibilities.
Experiment with these techniques in your projects—practice is key to internalizing them. For more, explore Python’s official docs or dive into advanced topics like metaclasses or asyncio!