Best Practices
## Learning Objectives
- Write clean, maintainable Python code
- Follow PEP 8 style guidelines
- Master debugging techniques
- Write effective tests
## Code Style
### PEP 8 Guidelines
- Use 4 spaces per indentation level
- Lines should be ≤ 79 characters (soft limit)
- Two blank lines between top-level definitions
- One blank line between method definitions in a class
- No spaces around `=` for keyword arguments or default values
```python
# Good
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# Bad
def greet ( name, greeting = "Hello" ) :
return greeting + ", " + name + "!"
```
### Naming Conventions
```python
# Variables and functions: snake_case
user_name = "Alice"
def calculate_total():
pass
# Classes: PascalCase
class UserAccount:
pass
# Constants: UPPER_SNAKE_CASE
MAX_RETRIES = 3
API_URL = "https://api.example.com"
# Private: prefix with underscore
_private_function()
_instance_variable
```
### Imports
```python
# Standard library first
import os
import sys
# Third-party
import requests
from flask import Flask
# Local application
from mypackage import mymodule
# Absolute vs Relative
from package import module # Preferred
from . import module # OK for relative
import module # Avoid relative
```
## Functions
### Single Responsibility
```python
# Good - each function does one thing
def validate_email(email):
"""Check if email is valid."""
return "@" in email and "." in email
def send_email(recipient, message):
"""Send an email."""
if not validate_email(recipient):
raise ValueError("Invalid email")
# Send logic...
# Bad - function does too much
def send_email(recipient, message):
# Validates, sends, logs, retries...
```
### Early Returns
```python
# Good - early return for guard clauses
def process(user):
if user is None:
return None
if not user.is_active:
return None
return user.process()
# Bad - deeply nested
def process(user):
if user is not None:
if user.is_active:
return user.process()
else:
return None
else:
return None
```
### Docstrings
```python
def add(a, b):
"""
Add two numbers.
Args:
a: First number
b: Second number
Returns:
The sum of a and b
Raises:
TypeError: If a or b is not a number
"""
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Arguments must be numbers")
return a + b
```
## Classes
### Use Properties
```python
# Good - use property for controlled access
class BankAccount:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
```
### dataclasses for Simple Classes
```python
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance_from_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
```
## Error Handling
### Specific Exceptions
```python
# Good
try:
value = int(user_input)
except ValueError:
print("Please enter a valid number")
# Bad - catches everything
try:
value = int(user_input)
except:
print("Error")
```
### Exception Hierarchy
```python
# Create specific exceptions
class ValidationError(Exception):
pass
class InvalidEmailError(ValidationError):
pass
class InvalidPasswordError(ValidationError):
pass
```
### Don't Suppress Exceptions
```python
# Bad
try:
do_something()
except:
pass
# Good - at minimum, log it
try:
do_something()
except Exception as e:
logging.error(f"Failed: {e}")
raise
```
## Type Hints
```python
from typing import List, Dict, Optional, Union
def process(numbers: List[int]) -> Dict[str, int]:
return {
"count": len(numbers),
"sum": sum(numbers),
}
def find_user(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "User found"
def parse(value: Union[str, int]) -> int:
return int(value)
```
## Performance
### List Comprehensions over Loops
```python
# Good
squares = [x ** 2 for x in range(100)]
# Bad
squares = []
for x in range(100):
squares.append(x ** 2)
```
### Generators for Large Data
```python
# Good - memory efficient
def read_large_file(filename):
with open(filename) as f:
for line in f:
yield line
# Bad - loads entire file
def read_large_file(filename):
with open(filename) as f:
return f.readlines()
```
### Local Variable Caching
```python
import math
# Slower - repeated attribute lookup
def compute(numbers):
result = []
for num in numbers:
result.append(math.sin(num) + math.cos(num))
return result
# Faster - cache math functions
def compute(numbers):
result = []
sin, cos = math.sin, math.cos
for num in numbers:
result.append(sin(num) + cos(num))
return result
```
## Testing Basics
### Using unittest
```python
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
def test_divide(self):
self.assertRaises(ZeroDivisionError, lambda: 1 / 0)
if __name__ == "__main__":
unittest.main()
```
### Using pytest
```python
import pytest
def test_add():
assert 1 + 1 == 2
def test_divide():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_list_comprehension():
result = [x ** 2 for x in range(5)]
assert result == [0, 1, 4, 9, 16]
```
## Debugging
### print() Debugging
```python
def buggy_function(x):
print(f"DEBUG: x = {x}") # Add debug output
return x * 2
```
### assert
```python
def process(age):
assert age >= 0, "Age cannot be negative"
assert isinstance(age, int), "Age must be an integer"
# Process...
```
### Using pdb
```python
import pdb
def buggy_function(x):
pdb.set_trace() # Breakpoint
return x * 2
```
## Summary
- Follow PEP 8 style guidelines
- Use meaningful variable and function names
- Keep functions small and focused
- Use type hints for documentation
- Handle exceptions specifically
- Use list comprehensions over loops
- Use generators for large datasets
- Write tests for critical code
- Use assertions for debugging
- Keep code DRY (Don't Repeat Yourself)
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →