40. Writing Clean and Readable Code
Throughout this book, you've learned Python's syntax, data structures, control flow, functions, classes, and many other programming concepts. You can now write programs that work. But there's a crucial difference between code that works and code that is maintainable—code that you and others can understand, modify, and debug months or years later.
This chapter focuses on writing clean, readable code. You'll learn the conventions and practices that make Python code professional and maintainable. These aren't just arbitrary rules—they're battle-tested guidelines that make collaboration easier, reduce bugs, and help you understand your own code when you return to it later.
40.1) Why Style Matters: Reading vs. Writing Code
40.1.1) Code Is Read More Often Than Written
When you write code, you spend minutes or hours creating it. But that code will be read many times: when you debug it, when you add features, when other developers work with it, and when you return to it months later trying to remember what it does.
Consider this working but poorly styled code:
# WARNING: Poor style - for demonstration only
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6This code works perfectly. It calculates the average of a list of numbers. But understanding what it does requires careful analysis. Now compare it to this version:
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.6What makes the second version better?
- Function name (
calculate_average) clearly states the purpose - Variable names (
numbers,total,test_scores) are descriptive - Docstring explains what the function does
- Proper spacing makes the structure clear
- Anyone can understand this code without studying it
Both versions produce identical results, but the second version is immediately understandable.
The key insight: You write code once, but you read it dozens or hundreds of times. Investing a few extra seconds in clear naming and formatting saves hours of confusion later.
40.1.2) Readability Reduces Bugs
Clear code is easier to debug because you can quickly understand what each part does. When variable names are descriptive and the structure is clean, you can spot logic errors more easily.
# Hard to debug - what do these variables represent?
# WARNING: Poor style - for demonstration only
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# Easy to debug - clear what's happening
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # 10% discount
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0In the second version, you can immediately see the logic: "We're calculating a discount amount, then subtracting it from the price." In the first version, you have to mentally track what x and y represent and figure out what x * (1 - y) means.
40.1.3) Consistency Enables Collaboration
When everyone on a team follows the same style conventions, code becomes predictable. You don't waste mental energy deciphering different formatting styles—you can focus on understanding the logic.
Python has an official style guide called PEP 8 (Python Enhancement Proposal 8). PEP 8 defines conventions for:
- How to name variables, functions, and classes
- How to format code (spacing, line length, indentation)
- When to use comments and docstrings
- How to organize imports
Following PEP 8 means your code will look familiar to other Python programmers, making collaboration smoother. We'll cover the essential PEP 8 guidelines in the next sections.
40.2) Naming Conventions: Variables, Functions, and Classes (PEP 8)
40.2.1) General Naming Principles
Good names are descriptive and unambiguous. They should tell you what something represents or does without requiring you to read the implementation.
Key principles:
- Use complete words, not abbreviations (except for very common ones like
id,url,html) - Be specific:
user_countis better thancount,calculate_total_priceis better thancalculate - Avoid single-letter names except for very short loops or mathematical formulas
- Don't include type information in names (Python is dynamically typed)
# Poor names - unclear what they represent
# WARNING: Poor style - for demonstration only
# What is 'n'? A number? A name? A node?
# What is 'd'? A date? A distance? A duration?
# What is 'l'? Looks like number 1!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# Good names - clear and descriptive
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2Exception: Short loop variables
# Acceptable: very short, clear context
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# But prefer descriptive names for clarity
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) Variable and Function Names: snake_case
In Python, variables and functions use snake_case: all lowercase with words separated by underscores.
# Variables
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# Functions
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# Using the functions
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")Why snake_case? It's highly readable. The underscores create clear word boundaries, making names easy to scan. Compare calculatetotalprice (hard to read) with calculate_total_price (immediately clear).
40.2.3) Constant Names: UPPER_SNAKE_CASE
Constants—values that shouldn't change during program execution—use UPPER_SNAKE_CASE: all uppercase with underscores.
# Constants at module level
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # Constant within function
return len(password) >= MIN_PASSWORD_LENGTH
# Using constants
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")Important: Python has no built-in constant syntax. Unlike some languages (like const in JavaScript or final in Java), Python doesn't have a way to declare that a variable cannot be changed.
Instead, Python programmers use a naming convention to signal intent:
UPPER_SNAKE_CASEmeans: "I intend this to be a constant—don't modify it"- It's a communication tool between programmers, not a language feature
# Python has no constant syntax - this is just a regular variable
MAX_LOGIN_ATTEMPTS = 3
# Python won't stop you from modifying it
MAX_LOGIN_ATTEMPTS = 5 # ❌ Technically works, but violates convention
# The naming convention is a signal about INTENT:
# "I named this in UPPERCASE to show I don't want it changed"Best practice: If a value truly needs to change during program execution, don't name it as a constant:
# This value will change - use lowercase
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK - name indicates it can change
# This value should never change - use UPPERCASE
MAX_LOGIN_ATTEMPTS = 3
# Don't reassign it later in the codeThe convention helps programmers understand your intent and avoid bugs. When you see MAX_LOGIN_ATTEMPTS, you know not to modify it.
40.2.4) Class Names: PascalCase
Class names use PascalCase (also called CapWords): each word starts with a capital letter, no underscores.
# Class definitions
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# Creating instances (note: instances use snake_case variable names)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")Why PascalCase for classes? It visually distinguishes classes from functions and variables. When you see Student(), you immediately know it's creating an instance of a class. When you see calculate_average(), you know it's calling a function.
40.2.5) Private and Internal Names: Leading Underscore
Names starting with a single underscore (_name) indicate internal use—they're meant for use within the module or class, not by external code.
Python has no syntax to mark methods or attributes as "private" (unlike private in Java or C++). Instead, Python uses a naming convention with a leading underscore (_name) to communicate intent.
What _name means:
- "This is for internal use only"
- "I made this for use within this class/module, not for external code"
- "This might change at any time in future versions—don't depend on it"
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # Internal attribute
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # Internal method
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# Using the class
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# Technically works, but violates the convention
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# Technically works, but violates the convention
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)Key point: Python cannot prevent you from accessing _balance or calling _validate_amount(). The underscore is a signal between programmers, not a security feature.
Why This Convention Exists
Since Python can't enforce privacy, the underscore is how class authors communicate their intent:
What the underscore signals:
- "This is internal implementation—it might change in future versions"
- "Use the public methods instead—they're guaranteed to remain stable"
- "If you depend on internal details, your code might break when I update the library"
The convention creates a contract: class authors can freely change internal implementation (anything with _), but must keep the public interface stable. This lets libraries evolve without breaking user code.
40.2.6) Special Names: Double Underscores
Names with double leading and trailing underscores (__name__) are special methods or magic methods defined by Python. Don't create your own names with this pattern—it's reserved for Python's use.
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # Special method: initialization
self.x = x
self.y = y
def __str__(self): # Special method: string representation
return f"Point({self.x}, {self.y})"
def __add__(self, other): # Special method: addition operator
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)As we learned in Chapter 31, these special methods enable operator overloading and integration with Python's built-in functions.
40.2.7) Naming Summary Table
| Type | Convention | Example |
|---|---|---|
| Variables | snake_case | user_name, total_count |
| Functions | snake_case | calculate_tax(), send_email() |
| Constants | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| Classes | PascalCase | Student, ShoppingCart |
| Internal/Private | _leading_underscore | _balance, _validate() |
| Special/Magic | double_underscore | __init__, __str__ |
40.3) Code Layout: Indentation, Spacing, and Blank Lines
40.3.1) Indentation: Four Spaces
Python uses indentation to define code blocks. Always use 4 spaces per indentation level—never tabs, and never mix tabs and spaces.
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# Nested indentation: 4 spaces per level
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: CWhy 4 spaces? It's the Python community standard. Most Python code you encounter uses 4 spaces, so following this convention makes your code consistent with the ecosystem.
Configuring your editor: Modern code editors can be set to insert 4 spaces when you press Tab. This gives you the convenience of the Tab key while maintaining the 4-space standard.
40.3.2) Maximum Line Length: 79 Characters
PEP 8 recommends limiting lines to 79 characters (with up to 99 characters for docstrings and comments). This might seem restrictive, but it has practical benefits:
- Code remains readable on smaller screens
- You can view two files side-by-side
- It encourages breaking complex expressions into simpler parts
Note: Many modern projects use slightly longer limits (88, 100, or 120 characters). The key is consistency within your project. Choose a limit and stick to it.
# Too long - hard to read
# WARNING: Poor style - for demonstration only
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# Better - broken into readable lines
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37Breaking long lines: When you need to break a line, use implicit line continuation inside parentheses, brackets, or braces:
# Long function call
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# Long list
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# Long string
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) Spacing Around Operators and After Commas
Use spaces around operators and after commas to improve readability:
# Poor spacing - cramped and hard to read
# WARNING: Poor style - for demonstration only
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# Good spacing - clear and readable
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# Spacing in expressions
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# Spacing in function definitions
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return priceException: Don't use spaces around = in keyword arguments or default parameter values:
# Correct spacing for keyword arguments
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# Correct spacing for default parameters
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) Blank Lines for Logical Separation
Use blank lines to separate logical sections of code:
Two blank lines between top-level functions and classes:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
passOne blank line between methods inside a class:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)Blank lines within functions to separate logical steps:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# Calculate subtotal
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# Apply customer discount
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# Calculate tax
tax = (subtotal - discount) * 0.08
# Calculate final total
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}These blank lines act as visual "paragraphs," making the code structure immediately apparent.
40.3.5) Avoiding Trailing Whitespace
Don't leave spaces at the end of lines—they're invisible but can cause issues with version control systems and some editors.
# Bad - invisible trailing spaces (shown as · for illustration)
# WARNING: Poor style - for demonstration only
def calculate(x):···
return x * 2···
# Good - no trailing spaces
def calculate(x):
return x * 2Most modern editors can be configured to automatically remove trailing whitespace when you save a file.
40.4) Documentation: Writing Helpful Comments and Docstrings
40.4.1) When to Write Comments
Comments explain why code does something, not what it does. Well-named variables and functions should make the "what" obvious.
# Poor comment - states the obvious
# WARNING: Poor style - for demonstration only
x = x + 1 # Add 1 to x
# Good comment - explains why
x = x + 1 # Adjust for zero-based indexing
# Poor comment - redundant with code
# WARNING: Poor style - for demonstration only
# Check if age is greater than or equal to 18
if age >= 18:
print("Adult")
# Good comment - explains business logic
# Legal drinking age in the US
if age >= 21:
print("Can purchase alcohol")When comments are valuable:
- Explaining complex algorithms:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# Calculate middle point, avoiding integer overflow
# (right + left) // 2 could overflow with very large indices
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # Target is in right half
else:
right = mid - 1 # Target is in left half
return -1 # Target not found- Clarifying non-obvious business rules:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# Free shipping promotion for heavy items (company policy as of 2024)
# This encourages bulk orders and reduces per-unit shipping costs
if weight > 50:
return 0
# Standard rate: $0.50 per pound plus $0.10 per mile
# Based on carrier contract negotiated in Q1 2024
return base_cost + (weight * 0.50) + (distance * 0.10)- Documenting workarounds or temporary solutions:
def process_data(data):
"""Process incoming data records."""
# TODO: This is a temporary fix for malformed records
# Remove once data validation is implemented upstream
if not isinstance(data, list):
data = [data]
for record in data:
# Process each record
pass40.4.2) Writing Effective Docstrings
Docstrings are special comments that document modules, classes, and functions. They're enclosed in triple quotes and appear as the first statement in the definition.
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# Accessing docstrings
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...One-line docstrings for simple functions:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0Multi-line docstrings for complex functions:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factorsClass docstrings:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) Docstring Conventions
First line: Brief summary of what the function/class does. Should fit on one line.
Blank line: Separate the summary from the detailed description.
Detailed description: Explain what the function does, any important details, and how to use it.
Args/Parameters: List each parameter with its type and purpose.
Returns: Describe what the function returns and its type.
Raises: Document any exceptions the function might raise.
Example: Show typical usage (optional but helpful).
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) TODO Comments for Future Work
Use TODO comments to mark areas that need future attention:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: Add support for cryptocurrency payments
# TODO: Implement fraud detection checks
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")Many editors can search for TODO comments, making it easy to find areas that need work.
40.5) Organizing Your Code: Imports, Constants, Functions, and Main
40.5.1) Standard Module Structure
A well-organized Python module follows this structure:
- Module docstring: Describes what the module does
- Imports: Standard library, third-party, then local imports
- Constants: Module-level constants
- Functions and classes: Main code
- Main execution block: Code that runs when the script is executed
"""
student_manager.py
Manage student records including grades and GPA calculations.
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
# Standard library imports
import sys
from datetime import datetime
# Third-party imports (if any)
# import requests
# Local imports (if any)
# from .database import save_student
# Constants
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# Functions
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# Convert to 4.0 scale
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# Main execution
if __name__ == "__main__":
# Code that runs when script is executed directly
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) Import Organization
Group imports into three sections, separated by blank lines:
- Standard library imports: Built-in Python modules
- Third-party imports: Installed packages (like
requests,numpy) - Local imports: Your own modules
# Standard library imports
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# Third-party imports
import requests
from flask import Flask, render_template
# Local application imports
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyImport styles:
# Import entire module
import math
result = math.sqrt(16) # Output: 4.0
# Import specific items
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# Import with alias
import numpy as np
array = np.array([1, 2, 3])
# Import multiple items
from os import path, getcwd, listdirAvoid wildcard imports (from module import *)—they make it unclear where names come from:
# Poor - unclear where sqrt comes from
# WARNING: Poor style - for demonstration only
from math import *
result = sqrt(16)
# Good - explicit import
from math import sqrt
result = sqrt(16)40.5.3) Organizing Constants
Place module-level constants near the top, after imports:
"""Configuration settings for the application."""
import os
# Application constants
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# Database configuration
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# Business rules
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) Logical Function Ordering
Organize functions in a logical order:
- Public functions first: Functions meant to be used by other modules
- Helper functions after: Internal functions that support public ones
- Related functions together: Group functions that work together
"""Order processing module."""
# Public API functions
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# Internal helper functions
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)Notice how the public functions (process_order, validate_order) come first, and the helper functions (prefixed with _) come after. This makes it clear which functions are the main API.
40.5.5) Class Organization Within Modules
When a module contains classes, organize them logically:
"""User management system."""
# Constants
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# Base classes first
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# Derived classes after base classes
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# Related classes grouped together
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# Utility functions related to classes
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)Class organization principles:
- Base classes before derived classes (readers need to understand the base first)
- Related classes grouped together (User and Resource are related)
- Utility functions that work with classes come after the class definitions
- Each class should have a clear docstring explaining its purpose
40.6) The if name == "main" Pattern
40.6.1) Understanding the Pattern
Every Python file has a built-in variable called __name__. Python automatically sets this variable's value depending on how the file is being used:
- When you run a file directly (e.g.,
python my_script.py), Python sets__name__to"__main__" - When you import a file as a module, Python sets
__name__to the module's name (the filename without.py)
This allows you to write code that only runs when the file is executed directly, not when it's imported:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# This code only runs when the file is executed directly
if __name__ == "__main__":
# Test the functions
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15When you run python math_utils.py, you'll see the output. But when you import it in another file:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# The test code from math_utils.py does NOT runNotice that the test code (inside if __name__ == "__main__":) does NOT run when imported!
40.6.2) Why This Pattern Matters
This pattern serves several important purposes:
1. Testing and demonstration: You can include example usage in the same file as your functions:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# Demonstrate the functions
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. Reusable modules: The same file can be both a standalone script and an importable module:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# When run as a script, process command-line arguments
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")You can run this as a script:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23Or import it in another file:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) Common Patterns for Main Blocks
Pattern 1: Simple test cases
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# Quick tests
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!Pattern 2: Main function
For more complex scripts, define a main() function:
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementation here
pass
def generate_report(data):
"""Generate report from data."""
# Implementation here
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementation here
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# Exit with status code from main (0 = success, 1 = error)
sys.exit(main())This pattern has several advantages:
- The
main()function can be tested independently - Clear entry point for the script
- Proper exit codes (0 for success, non-zero for errors)
- Clean separation between script logic and module functions
40.6.4) Best Practices for Main Blocks
Keep main blocks focused: The code inside if __name__ == "__main__" should primarily handle script execution, not contain complex logic:
# Poor - complex logic in main block
# WARNING: Poor style - for demonstration only
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# Good - logic in functions, main block coordinates
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0Use main() function for complex scripts: As shown earlier, defining a main() function makes your script more testable and organized.
Document script usage: If your script accepts command-line arguments, document them in the module docstring:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # Print the module docstring
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")Writing clean, readable code is a skill that develops with practice. The conventions and patterns in this chapter aren't arbitrary rules—they're proven practices that make code easier to understand, maintain, and debug. As you write more Python code, these patterns will become second nature.
Remember: code is read far more often than it's written. The few extra seconds you spend choosing a clear name, adding a helpful comment, or organizing your imports properly will save hours of confusion later—for yourself and others who work with your code.
In the next chapter, we'll explore debugging and testing techniques that build on these clean code practices, helping you write not just readable code, but correct and reliable code.