Python Idioms
Python has many idiomatic patterns that make code more readable, maintainable, and “Pythonic.” Here are the essential idioms every new Python developer should know.
The if __name__ == "__main__"
Pattern
This is one of the most important Python idioms you’ll encounter:
def main():
print("This script is being run directly")
# Your main program logic here
if __name__ == "__main__":
main()
Why Use This Pattern?
When Python runs a file, it sets the __name__
variable:
- If the file is run directly (e.g.,
python script.py
),__name__
equals"__main__"
- If the file is imported as a module,
__name__
equals the module name
This pattern allows your script to:
- Be both a script and a module - Code can be imported without executing the main logic
- Prevent unwanted execution - When someone imports your module, the main code won’t run
- Enable testing - You can import functions for testing without side effects
Example:
# calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def main():
print("Calculator running...")
result = add(5, 3)
print(f"5 + 3 = {result}")
if __name__ == "__main__":
main()
Now you can either run it directly (python calculator.py
) or import it (from calculator import add
).
Setting Up a Proper Python Environment
Use Virtual Environments
Always use virtual environments to isolate project dependencies:
# Create a virtual environment
python -m venv myproject_env
# Activate it (macOS/Linux)
source myproject_env/bin/activate
# Activate it (Windows)
myproject_env\Scripts\activate
# Install packages
pip install requests pandas
# Save dependencies
pip freeze > requirements.txt
# Deactivate when done
deactivate
Project Structure
Organize your projects with a standard structure:
myproject/
├── README.md
├── requirements.txt
├── setup.py (optional)
├── src/
│ └── myproject/
│ ├── __init__.py
│ ├── main.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ └── test_main.py
└── docs/
Essential Python Idioms
List Comprehensions
Replace verbose loops with concise list comprehensions:
# Instead of this:
squares = []
for i in range(10):
squares.append(i ** 2)
# Use this:
squares = [i ** 2 for i in range(10)]
# With conditions:
even_squares = [i ** 2 for i in range(10) if i % 2 == 0]
Dictionary Comprehensions
Create dictionaries efficiently:
# Create a dictionary from two lists
keys = ['a', 'b', 'c']
values = [1, 2, 3]
my_dict = {k: v for k, v in zip(keys, values)}
# Transform existing dictionary
prices = {'apple': 0.50, 'banana': 0.30, 'orange': 0.75}
expensive = {fruit: price for fruit, price in prices.items() if price > 0.40}
Enumerate Instead of Range(len())
items = ['apple', 'banana', 'cherry']
# Instead of this:
for i in range(len(items)):
print(f"{i}: {items[i]}")
# Use this:
for i, item in enumerate(items):
print(f"{i}: {item}")
Zip for Parallel Iteration
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
# Instead of indexing:
for i in range(len(names)):
print(f"{names[i]} is {ages[i]} years old")
# Use zip:
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
Context Managers (with statements)
Always use context managers for resource management:
# File handling
with open('file.txt', 'r') as f:
content = f.read()
# File automatically closed
# Custom context managers
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield
end = time.time()
print(f"Elapsed: {end - start:.2f} seconds")
with timer():
# Your code here
time.sleep(1)
String Formatting
Use f-strings (Python 3.6+) for readable string formatting, see string formatting cookbook for more:
name = "Alice"
age = 30
# Old ways (avoid):
message = "Hello, %s! You are %d years old." % (name, age)
message = "Hello, {}! You are {} years old.".format(name, age)
# Modern way:
message = f"Hello, {name}! You are {age} years old."
# With expressions:
price = 19.99
message = f"Total: ${price * 1.08:.2f}" # Includes tax, 2 decimal places
Default Dictionary Values
Use dict.get()
or collections.defaultdict
:
# Instead of checking if key exists:
if 'key' in my_dict:
value = my_dict['key']
else:
value = 'default'
# Use get():
value = my_dict.get('key', 'default')
# For counting or grouping:
from collections import defaultdict
# Count items
counter = defaultdict(int)
for item in items:
counter[item] += 1
# Group items
groups = defaultdict(list)
for item in items:
groups[item.category].append(item)
Unpacking and Multiple Assignment
# Tuple unpacking
point = (3, 4)
x, y = point
# Swap variables
a, b = b, a
# Extended unpacking (Python 3+)
first, *middle, last = [1, 2, 3, 4, 5]
# first = 1, middle = [2, 3, 4], last = 5
# Function arguments
def greet(name, age, city):
return f"Hi {name}, {age} years old from {city}"
person_data = ("Alice", 30, "New York")
message = greet(*person_data) # Unpacking tuple
person_dict = {"name": "Bob", "age": 25, "city": "Boston"}
message = greet(**person_dict) # Unpacking dictionary
Exception Handling Best Practices
# Be specific with exceptions
try:
value = int(user_input)
except ValueError:
print("Please enter a valid number")
except KeyboardInterrupt:
print("Operation cancelled")
# Use else and finally
try:
file = open('data.txt')
except FileNotFoundError:
print("File not found")
else:
# Runs only if no exception occurred
data = file.read()
finally:
# Always runs
if 'file' in locals():
file.close()
# EAFP (Easier to Ask for Forgiveness than Permission)
try:
return my_dict[key]
except KeyError:
return default_value
Generator Expressions and Functions
Use generators for memory-efficient iteration:
# Generator expression (like list comprehension but lazy)
squares_gen = (i ** 2 for i in range(1000000)) # Memory efficient
# Generator function
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Use generators
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]
Pathlib for File Operations
Use pathlib
instead of os.path
, see files article for more:
from pathlib import Path
# Instead of os.path
import os
file_path = os.path.join('data', 'files', 'input.txt')
if os.path.exists(file_path):
with open(file_path, 'r') as f:
content = f.read()
# Use pathlib
file_path = Path('data') / 'files' / 'input.txt'
if file_path.exists():
content = file_path.read_text()
# More pathlib examples
current_dir = Path.cwd()
parent_dir = Path(__file__).parent
all_py_files = list(Path('.').glob('**/*.py'))
Type Hints (Python 3.5+)
Add type hints for better code documentation and IDE support, see type hints for more:
from typing import List, Dict, Optional, Union
def process_items(items: List[str], multiplier: int = 1) -> List[str]:
"""Process a list of items."""
return [item * multiplier for item in items]
def get_user_data(user_id: int) -> Optional[Dict[str, Union[str, int]]]:
"""Get user data, returns None if not found."""
# Implementation here
pass
Truthy and Falsy Values
Understand how Python evaluates values in a boolean context:
# Falsy values:
# - None
# - False
# - Zero of any numeric type (0, 0.0, 0j)
# - Empty sequences (list, tuple, string: [], (), '')
# - Empty mappings (dictionary: {})
# - Instances of user-defined classes, if the class defines a __bool__() or __len__() method that returns False or 0.
my_list = []
if not my_list: # More Pythonic than if len(my_list) == 0:
print("List is empty")
my_string = "Hello"
if my_string: # True because it's not empty
print(f"String: {my_string}")
Iterable Slicing
Work with parts of sequences efficiently:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
first_three = numbers[:3] # [0, 1, 2]
last_three = numbers[-3:] # [7, 8, 9]
middle = numbers[2:7] # [2, 3, 4, 5, 6]
every_other = numbers[::2] # [0, 2, 4, 6, 8]
reverse_list = numbers[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# Slicing works on strings too
text = "Pythonic"
print(text[1:4]) # yth
Use Underscore as a Throwaway Variable
Use _
for variables whose values you don’t intend to use:
# When unpacking, if you only need certain values
coordinates = (10, 20, 5)
x, _, z = coordinates # y is ignored
# In loops where the index or value is not needed
for _ in range(5):
print("Hello")
Use any()
and all()
Check for conditions in iterables concisely:
numbers = [1, 2, 3, -5, 6]
# Check if any number is negative
has_negative = any(n < 0 for n in numbers)
print(f"Has negative numbers: {has_negative}") # True
# Check if all numbers are positive
all_positive = all(n > 0 for n in numbers)
print(f"All numbers positive: {all_positive}") # False
Code Style and Standards
Follow PEP 8
Use tools to enforce Python’s style guide:
# Install formatting and linting tools
pip install black flake8 isort mypy
# Format code
black your_script.py
# Sort imports
isort your_script.py
# Check style
flake8 your_script.py
# Type checking
mypy your_script.py
Naming Conventions
# Variables and functions: snake_case
user_name = "Alice"
def calculate_total_price():
pass
# Constants: UPPER_SNAKE_CASE
MAX_CONNECTIONS = 100
API_BASE_URL = "https://api.example.com"
# Classes: PascalCase
class UserAccount:
pass
# Private attributes: leading underscore
class MyClass:
def __init__(self):
self._private_var = "internal use"
self.__very_private = "name mangled"
Documentation
Write clear docstrings:
def calculate_area(length: float, width: float) -> float:
"""Calculate the area of a rectangle.
Args:
length: The length of the rectangle
width: The width of the rectangle
Returns:
The area of the rectangle
Raises:
ValueError: If length or width is negative
"""
if length < 0 or width < 0:
raise ValueError("Length and width must be non-negative")
return length * width
Testing Your Code
Write tests using pytest:
# test_calculator.py
import pytest
from calculator import add, subtract
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_subtract():
assert subtract(5, 3) == 2
assert subtract(0, 5) == -5
def test_add_with_floats():
assert add(0.1, 0.2) == pytest.approx(0.3)
# Run tests
# pytest test_calculator.py