Skip to content

Python Best Practices

This guide covers essential Python programming best practices for writing clean, efficient, and maintainable code following PEP 8 and Pythonic conventions.

🎯 Core Python Best Practices

Naming Conventions (PEP 8)

  • Variables/Functions: snake_case (e.g., student_name, calculate_total())
  • Classes: PascalCase (e.g., StudentRecord, DatabaseManager)
  • Constants: UPPER_SNAKE_CASE (e.g., MAX_RETRY_ATTEMPTS, DEFAULT_TIMEOUT)
  • Private Members: Leading underscore (e.g., _internal_method, __private_var)
  • Modules: lowercase with underscores (e.g., student_utils.py)

Code Organization

  • Docstrings: Use triple quotes for function/class documentation
  • Type Hints: Add type annotations for better code clarity
  • Import Organization: Group imports (standard library, third-party, local)
  • Function Length: Keep functions short and focused on single responsibility

Pythonic Idioms

  • List Comprehensions: Prefer over explicit loops
  • Context Managers: Use with for resource management
  • Generators: Use yield for memory-efficient iteration
  • Decorators: Use for cross-cutting concerns

🔧 Python-Specific Techniques

Input/Output

# Good: Using context managers and exception handling
def get_user_input():
    """Get user input with proper error handling."""
    try:
        with open('data.txt', 'r') as file:
            return file.read()
    except FileNotFoundError:
        print("File not found, using default value")
        return "default"
    except IOError as e:
        print(f"Error reading file: {e}")
        return None

# Bad: No error handling
def get_user_input_bad():
    file = open('data.txt', 'r')
    return file.read()  # File never closed!

List Operations

# Good: List comprehensions and built-in functions
numbers = [1, 2, 3, 4, 5]
squares = [x**2 for x in numbers]  # List comprehension
even_numbers = [x for x in numbers if x % 2 == 0]  # Filtering
total = sum(numbers)  # Built-in function

# Bad: Manual loops for simple operations
squares_bad = []
for x in numbers:
    squares_bad.append(x**2)

total_bad = 0
for x in numbers:
    total_bad += x

String Handling

# Good: f-strings and string methods
name = "John"
age = 25
message = f"My name is {name} and I'm {age} years old"

# Good: String methods for manipulation
text = "  Hello World  "
cleaned = text.strip().lower().replace(' ', '_')

# Bad: String concatenation with +
message_bad = "My name is " + name + " and I'm " + str(age) + " years old"

Function Design

# Good: Type hints, docstrings, single responsibility
from typing import List, Optional

def calculate_average(numbers: List[float]) -> Optional[float]:
    """
    Calculate the average of a list of numbers.

    Args:
        numbers: List of numeric values

    Returns:
        Average value or None if list is empty
    """
    if not numbers:
        return None

    return sum(numbers) / len(numbers)

# Bad: No type hints, unclear purpose
def calc(data):
    # What does this function do?
    return sum(data) / len(data)

⚠️ Common Python Pitfalls

Mutable Default Arguments

# Bad: Mutable default argument
def add_item(item, items=[]):  # Same list used across calls!
    items.append(item)
    return items

# Good: Use None as default
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Variable Scope Issues

# Bad: Modifying loop variable
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers)):
    if numbers[i] % 2 == 0:
        numbers.remove(i)  # Modifying list while iterating!

# Good: Create new list or use list comprehension
numbers = [1, 2, 3, 4, 5]
even_numbers = [x for x in numbers if x % 2 == 0]

Comparison with None

# Bad: Using == for None comparison
if value == None:  # Should use 'is'
    pass

# Good: Use 'is' for identity comparison
if value is None:
    pass

# Better: Use truthiness for general cases
if not value:
    pass

🚀 Performance Optimization

Memory Efficiency

# Good: Generators for large datasets
def process_large_file(filename):
    """Process large file line by line."""
    with open(filename, 'r') as file:
        for line in file:  # Generator, memory efficient
            yield process_line(line)

# Bad: Loading entire file into memory
def process_large_file_bad(filename):
    with open(filename, 'r') as file:
        lines = file.readlines()  # Loads entire file!
    return [process_line(line) for line in lines]

Efficient Data Structures

# Good: Use appropriate data structures
# Set for membership testing (O(1))
valid_colors = {'red', 'green', 'blue'}
if color in valid_colors:  # Fast lookup
    pass

# Bad: List for membership testing (O(n))
valid_colors_bad = ['red', 'green', 'blue']
if color in valid_colors_bad:  # Slow lookup
    pass

String Operations

# Good: Join for string concatenation
words = ['hello', 'world', 'python']
sentence = ' '.join(words)  # Efficient

# Bad: Repeated string concatenation
sentence_bad = ''
for word in words:
    sentence_bad += word + ' '  # Creates new string each time

🛠️ Error Handling

Exception Handling Best Practices

# Good: Specific exceptions and proper cleanup
def read_config(filename):
    """Read configuration file with proper error handling."""
    try:
        with open(filename, 'r') as file:
            return json.load(file)
    except FileNotFoundError:
        logging.error(f"Config file not found: {filename}")
        return {}
    except json.JSONDecodeError as e:
        logging.error(f"Invalid JSON in config: {e}")
        return {}
    except Exception as e:
        logging.error(f"Unexpected error reading config: {e}")
        raise

# Bad: Catching all exceptions
def read_config_bad(filename):
    try:
        with open(filename, 'r') as file:
            return json.load(file)
    except:  # Too broad!
        return {}

📚 Testing Best Practices

Unit Testing with pytest

# Good: Descriptive test names and assertions
import pytest
from calculator import add

def test_add_positive_numbers():
    """Test adding two positive numbers."""
    result = add(2, 3)
    assert result == 5

def test_add_negative_numbers():
    """Test adding two negative numbers."""
    result = add(-2, -3)
    assert result == -5

def test_add_with_none_raises_exception():
    """Test that add raises TypeError for None input."""
    with pytest.raises(TypeError):
        add(None, 5)

📦 Project Structure

project_name/
├── src/
│   └── project_name/
│       ├── __init__.py
│       ├── main.py
│       └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_main.py
│   └── test_utils.py
├── docs/
├── requirements.txt
├── setup.py
└── README.md

Requirements Management

# requirements.txt
numpy>=1.21.0
pandas>=1.3.0
requests>=2.25.0
pytest>=6.2.0

🔗 Common Programming Best Practices