Python Full Stack Web Developer Course

Week 3: Python Fundamentals (Part 2)

Friday Afternoon: Python Code Quality and Best Practices

The Craft of Writing Quality Python Code

Welcome to our session on Python code quality and best practices! Over the past three weeks, we've learned how to write Python code that works. Now we'll focus on writing Python code that excels—code that is not only functional but also readable, maintainable, and elegant.

As we prepare to dive into web development, establishing good coding habits becomes even more critical. Web applications are typically larger, more complex, and maintained by teams over longer periods than simple scripts. The practices we explore today will help you write code that can scale with your projects and be easily understood by your colleagues (and your future self).

Why Code Quality Matters

Analogy: Writing code is like building a house. Functional code is a house that doesn't leak when it rains—it meets the minimum requirements. Quality code is a house that's well-designed, energy-efficient, easy to maintain, and pleasant to live in. Both will keep you dry, but only one will stand the test of time.

High-quality code offers several substantial benefits:

Real-world Impact: Google estimates that they spend about 2 billion dollars a year maintaining their code. They've found that implementing code quality standards across their organization has dramatically reduced this cost. In web development specifically, high-quality code directly impacts user experience through faster load times, more reliable functionality, and quicker feature development.

The PEP 8 Style Guide

PEP 8 is Python's official style guide—a set of conventions for writing clean, readable Python code. While some rules may seem arbitrary, following them creates consistency that makes all Python code more accessible.

Key PEP 8 Guidelines

Indentation and Line Breaks

# Bad indentation (mixing tabs and spaces)
def bad_function():
	x = 1
    y = 2  # This line uses spaces instead of tabs
	return x + y

# Good indentation (consistent 4 spaces)
def good_function():
    x = 1
    y = 2
    return x + y

# Bad line breaks (after operators)
income = (gross_wages +
          taxable_interest +
          dividends)

# Good line breaks (before operators)
income = (gross_wages
          + taxable_interest
          + dividends)

Imports

# Bad import organization
from app.models import User
import random
from flask import Flask, request
import os
from module import *

# Good import organization
# Standard library imports
import os
import random

# Third-party imports
from flask import Flask, request

# Local application imports
from app.models import User

Naming Conventions

# Bad naming
def f(x):
    l = []
    for i in range(x):
        l.append(i * 2)
    return l

# Good naming
def generate_even_numbers(count):
    even_numbers = []
    for i in range(count):
        even_numbers.append(i * 2)
    return even_numbers

# Constants should be UPPER_SNAKE_CASE
MAX_LOGIN_ATTEMPTS = 5
DEFAULT_TIMEOUT_SECONDS = 30

# Classes should be PascalCase
class UserAuthenticator:
    pass

Whitespace

# Bad whitespace
x=5+3
function( argument1, argument2 )
[ 1, 2, 3 ]

# Good whitespace
x = 5 + 3
function(argument1, argument2)
[1, 2, 3]

# Use blank lines to separate logical sections
def process_user_data(user):
    # Validate user data
    if not user.is_valid():
        return False
    
    # Process demographic information
    process_demographics(user)
    
    # Process purchase history
    purchases = get_purchase_history(user)
    analyze_purchase_patterns(purchases)
    
    return True

Practical Tip: Use automated tools like black or autopep8 to format your code according to PEP 8 guidelines. This allows you to focus on writing code rather than formatting it manually.

Documentation Best Practices

Metaphor: If code is a complex machine, documentation is the instruction manual. Even the most well-designed machine is difficult to use without clear instructions, and even the most well-written code benefits from explanations of its purpose and behavior.

Effective Docstrings

Python's docstrings provide a standardized way to document modules, classes, functions, and methods.

def calculate_discount(price, discount_rate, max_discount=None):
    """
    Calculate the discounted price of an item.
    
    Args:
        price (float): The original price of the item
        discount_rate (float): The discount rate as a decimal (e.g., 0.2 for 20%)
        max_discount (float, optional): The maximum discount amount. Defaults to None.
    
    Returns:
        float: The discounted price
    
    Raises:
        ValueError: If price or discount_rate is negative
    
    Examples:
        >>> calculate_discount(100, 0.2)
        80.0
        >>> calculate_discount(100, 0.5, max_discount=30)
        70.0
    """
    if price < 0 or discount_rate < 0:
        raise ValueError("Price and discount rate must be non-negative")
    
    discount_amount = price * discount_rate
    
    if max_discount is not None:
        discount_amount = min(discount_amount, max_discount)
    
    return price - discount_amount

Documentation Styles

There are several popular documentation styles in Python:

Google Style

def connect_to_database(host, user, password, database, port=3306):
    """Establishes a connection to the MySQL database.
    
    Args:
        host (str): The database server hostname or IP
        user (str): Username for authentication
        password (str): Password for authentication
        database (str): Name of the database to connect to
        port (int, optional): Server port. Defaults to 3306.
    
    Returns:
        Connection: A database connection object
    
    Raises:
        ConnectionError: If connection fails
    """

Numpy Style

def connect_to_database(host, user, password, database, port=3306):
    """
    Establishes a connection to the MySQL database.
    
    Parameters
    ----------
    host : str
        The database server hostname or IP
    user : str
        Username for authentication
    password : str
        Password for authentication
    database : str
        Name of the database to connect to
    port : int, optional
        Server port. Defaults to 3306.
    
    Returns
    -------
    Connection
        A database connection object
    
    Raises
    ------
    ConnectionError
        If connection fails
    """

reStructuredText (Sphinx) Style

def connect_to_database(host, user, password, database, port=3306):
    """Establishes a connection to the MySQL database.
    
    :param host: The database server hostname or IP
    :type host: str
    :param user: Username for authentication
    :type user: str
    :param password: Password for authentication
    :type password: str
    :param database: Name of the database to connect to
    :type database: str
    :param port: Server port, defaults to 3306
    :type port: int, optional
    
    :return: A database connection object
    :rtype: Connection
    
    :raises ConnectionError: If connection fails
    """

Best Practice: Choose one documentation style and stick with it consistently throughout your project. The Google style is often preferred for its readability and compact format.

Comments vs. Documentation

Understand the difference between comments and documentation:

# This is a GOOD comment because it explains WHY
# Skip validation for admin users because they operate under different security constraints
if not user.is_admin:
    validate_user_input(data)

# This is a BAD comment because it just restates what the code does
# Increment counter by 1
counter += 1

def process_payment(amount, payment_method):
    """
    Process a payment transaction.
    
    Args:
        amount (Decimal): The payment amount
        payment_method (PaymentMethod): The payment method to use
        
    Returns:
        TransactionID: The ID of the processed transaction
    """
    # Try the primary payment processor first, then fall back to the backup
    # if it fails (the primary is faster but occasionally has downtime)
    try:
        return primary_processor.process(amount, payment_method)
    except ProcessorUnavailable:
        return backup_processor.process(amount, payment_method)

Real-world Example: In a web application for a bank, proper documentation of the payment processing functions is critical. The docstrings would explain what each function does, its parameters, and return values, while comments would explain why certain security checks are performed or why specific error handling approaches were chosen.

Code Organization Principles

Analogy: Well-organized code is like a well-organized kitchen. Ingredients (data) and tools (functions) have their proper places, making the cooking process (execution) more efficient and less error-prone. When everything is in the right place, multiple chefs (developers) can work together without stepping on each other's toes.

Single Responsibility Principle

Each function, class, or module should have a single, well-defined responsibility.

# Bad organization: Function doing too many things
def process_user_signup(username, email, password):
    # Validate input
    if not username or not email or not password:
        raise ValueError("All fields are required")
    
    if '@' not in email:
        raise ValueError("Invalid email format")
    
    if len(password) < 8:
        raise ValueError("Password too short")
    
    # Check if user exists
    query = "SELECT * FROM users WHERE username = %s OR email = %s"
    cursor.execute(query, (username, email))
    if cursor.fetchone():
        raise ValueError("Username or email already exists")
    
    # Hash password
    salt = generate_salt()
    hashed_password = hash_password(password, salt)
    
    # Save to database
    query = """
        INSERT INTO users (username, email, password_hash, salt, created_at)
        VALUES (%s, %s, %s, %s, %s)
    """
    cursor.execute(query, (username, email, hashed_password, salt, datetime.now()))
    db_connection.commit()
    
    # Send welcome email
    send_email(
        to=email,
        subject="Welcome to our platform!",
        body=f"Hi {username}, thanks for signing up..."
    )
    
    return True

# Good organization: Single responsibility functions
def validate_signup_data(username, email, password):
    """Validate user signup data."""
    if not username or not email or not password:
        raise ValueError("All fields are required")
    
    if '@' not in email:
        raise ValueError("Invalid email format")
    
    if len(password) < 8:
        raise ValueError("Password too short")
    
    return True

def check_user_exists(username, email):
    """Check if username or email already exists."""
    query = "SELECT * FROM users WHERE username = %s OR email = %s"
    cursor.execute(query, (username, email))
    return cursor.fetchone() is not None

def hash_user_password(password):
    """Hash password with a new salt."""
    salt = generate_salt()
    hashed_password = hash_password(password, salt)
    return hashed_password, salt

def save_user_to_database(username, email, hashed_password, salt):
    """Save new user to database."""
    query = """
        INSERT INTO users (username, email, password_hash, salt, created_at)
        VALUES (%s, %s, %s, %s, %s)
    """
    cursor.execute(query, (username, email, hashed_password, salt, datetime.now()))
    db_connection.commit()
    return cursor.lastrowid

def send_welcome_email(username, email):
    """Send welcome email to new user."""
    send_email(
        to=email,
        subject="Welcome to our platform!",
        body=f"Hi {username}, thanks for signing up..."
    )

def process_user_signup(username, email, password):
    """Process a new user signup."""
    validate_signup_data(username, email, password)
    
    if check_user_exists(username, email):
        raise ValueError("Username or email already exists")
    
    hashed_password, salt = hash_user_password(password)
    user_id = save_user_to_database(username, email, hashed_password, salt)
    send_welcome_email(username, email)
    
    return user_id

DRY (Don't Repeat Yourself)

Avoid duplicating code by extracting repeated logic into reusable functions or classes.

# Violating DRY: Repeated validation logic
def validate_login(username, password):
    if not username:
        raise ValueError("Username is required")
    
    if not password:
        raise ValueError("Password is required")
    
    # More validation...

def validate_signup(username, email, password):
    if not username:
        raise ValueError("Username is required")
    
    if not password:
        raise ValueError("Password is required")
    
    if not email:
        raise ValueError("Email is required")
    
    # More validation...

# Following DRY: Reusable validation
def validate_required_fields(data, required_fields):
    """Validate that all required fields are present and non-empty."""
    for field in required_fields:
        if field not in data or not data[field]:
            raise ValueError(f"{field} is required")

def validate_login(data):
    validate_required_fields(data, ['username', 'password'])
    # Login-specific validation...

def validate_signup(data):
    validate_required_fields(data, ['username', 'email', 'password'])
    # Signup-specific validation...

YAGNI (You Aren't Gonna Need It)

Avoid adding functionality until it's actually necessary.

# Violating YAGNI: Implementing features "just in case"
class UserProfile:
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email
        self.preferences = {}
        self.social_links = {}
        self.favorite_products = []
        self.recently_viewed = []
        self.notification_settings = {
            'email': True,
            'sms': False,
            'push': False,
            'newsletter': True
        }
    
    def export_to_json(self):
        # JSON export functionality
        pass
    
    def export_to_xml(self):
        # XML export functionality
        pass
    
    def export_to_csv(self):
        # CSV export functionality
        pass

# Following YAGNI: Implementing only what's needed now
class UserProfile:
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email
    
    def to_dict(self):
        """Convert user profile to dictionary."""
        return {
            'user_id': self.user_id,
            'name': self.name,
            'email': self.email
        }

Separation of Concerns

Divide your code into distinct sections, each addressing separate concerns.

# Poor separation of concerns: Mixing business logic, data access, and presentation
def user_dashboard(user_id):
    # Data access
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    user = cursor.fetchone()
    
    cursor.execute("SELECT * FROM orders WHERE user_id = %s ORDER BY created_at DESC LIMIT 5", (user_id,))
    recent_orders = cursor.fetchall()
    
    # Business logic
    total_spent = sum(order['amount'] for order in recent_orders)
    if total_spent > 1000:
        user_status = "VIP"
    elif total_spent > 500:
        user_status = "Premium"
    else:
        user_status = "Regular"
    
    # Presentation
    html = f"

Welcome, {user['name']}!

" html += f"

Your status: {user_status}

" html += "

Recent Orders

" html += "
    " for order in recent_orders: html += f"
  • Order #{order['id']}: ${order['amount']} - {order['created_at']}
  • " html += "
" return html # Good separation of concerns # Data access layer def get_user(user_id): """Retrieve user from database.""" cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) return cursor.fetchone() def get_recent_orders(user_id, limit=5): """Retrieve recent orders for a user.""" cursor.execute( "SELECT * FROM orders WHERE user_id = %s ORDER BY created_at DESC LIMIT %s", (user_id, limit) ) return cursor.fetchall() # Business logic layer def calculate_user_status(orders): """Calculate user status based on order history.""" total_spent = sum(order['amount'] for order in orders) if total_spent > 1000: return "VIP" elif total_spent > 500: return "Premium" else: return "Regular" # Presentation layer (in Flask/Django/etc.) @app.route('/dashboard/') def user_dashboard(user_id): user = get_user(user_id) recent_orders = get_recent_orders(user_id) user_status = calculate_user_status(recent_orders) return render_template( 'dashboard.html', user=user, recent_orders=recent_orders, user_status=user_status )

Real-world Impact: In a web application, clear separation of concerns allows different team members to work on different parts of the codebase simultaneously. For example, one developer might improve the database access layer while another enhances the business logic, without stepping on each other's toes.

Function and Method Design

Metaphor: Functions are like specialized tools in a workshop. A well-designed tool is focused, reliable, easy to use, and has a clear purpose. Similarly, well-designed functions should be focused, reliable, easy to use, and have a clear purpose.

Function Length and Complexity

# Overly complex function
def process_order(order, user, promotion_code=None, shipping_method='standard', gift_wrap=False, use_store_credit=False):
    # 50+ lines of complex logic with many nested conditions
    # ...
    
# Better: Breaking down into focused functions
def apply_promotions(order, promotion_code=None):
    """Apply promotional discounts to order."""
    # Focus solely on promotions
    
def calculate_shipping(order, shipping_method='standard'):
    """Calculate shipping costs based on method."""
    # Focus solely on shipping
    
def process_order(order, user):
    """Process a complete order."""
    validate_order(order)
    
    apply_promotions(order, order.promotion_code)
    
    shipping_cost = calculate_shipping(order, order.shipping_method)
    order.total += shipping_cost
    
    if order.gift_wrap:
        apply_gift_wrap(order)
    
    if order.use_store_credit:
        apply_store_credit(order, user)
    
    return finalize_order(order, user)

Return Values and Side Effects

# Inconsistent return values and hidden side effects
def process_user(user):
    if not user.is_active:
        return False
    
    if user.needs_update:
        user.updated = True  # Side effect 1
        update_user_database(user)  # Side effect 2
    
    for group in user.groups:
        grant_permissions(user, group)  # Side effect 3
    
    active_users.append(user)  # Side effect 4

# More explicit about return values and side effects
def process_user(user):
    """
    Process a user account, updating records and granting permissions.
    
    This function has several side effects:
    - Updates the user record in the database if needed
    - Grants permissions based on user groups
    - Adds the user to the active_users list
    
    Args:
        user (User): The user to process
        
    Returns:
        bool: True if processing was successful, False otherwise
    """
    if not user.is_active:
        return False
    
    result = True
    
    # Clearly separated side effects
    if user.needs_update:
        user.updated = True
        result = result and update_user_database(user)
    
    # Return value captures success/failure of side effects
    for group in user.groups:
        result = result and grant_permissions(user, group)
    
    if result:
        active_users.append(user)
    
    return result

Function Arguments

# Difficult to use correctly
def create_report(id, type, start, end, format, include_chart, chart_type, width, height, compare):
    # Implementation...

# Function call is confusing
create_report(42, 'sales', '2023-01-01', '2023-03-31', 'pdf', True, 'bar', 800, 600, True)

# Better: Keyword arguments with defaults
def create_report(
    id,
    report_type,
    start_date,
    end_date,
    format='pdf',
    include_chart=False,
    chart_type='bar',
    chart_width=800,
    chart_height=600,
    compare_to_previous=False
):
    # Implementation...

# Function call is clearer
create_report(
    id=42,
    report_type='sales',
    start_date='2023-01-01',
    end_date='2023-03-31',
    include_chart=True
)

# Even better: Using a data class for complex parameters
from dataclasses import dataclass

@dataclass
class ReportOptions:
    format: str = 'pdf'
    include_chart: bool = False
    chart_type: str = 'bar'
    chart_width: int = 800
    chart_height: int = 600
    compare_to_previous: bool = False

def create_report(id, report_type, start_date, end_date, options=None):
    if options is None:
        options = ReportOptions()
    # Implementation...

Real-world Example: In a data analysis web application, well-designed functions for data processing make the codebase more testable and maintainable. For instance, separating data loading, cleaning, analysis, and visualization into distinct functions allows each component to be tested independently and reused in different contexts.

Effective Error Handling

Analogy: Error handling is like having emergency protocols in a hospital. Good protocols anticipate problems, provide clear guidance for addressing them, and maintain the overall system's stability even when things go wrong.

Principles of Effective Error Handling

# Poor error handling
def get_user_data(user_id):
    try:
        # This try/except is too broad
        return database.query(f"SELECT * FROM users WHERE id = {user_id}")
    except:
        # Silently ignoring errors is dangerous
        return None

# Better error handling
def get_user_data(user_id):
    try:
        # Parameterized query prevents SQL injection
        return database.query("SELECT * FROM users WHERE id = %s", (user_id,))
    except ConnectionError as e:
        # Log specific errors
        logger.error(f"Database connection error: {e}")
        raise ServiceUnavailableError("Database service is unavailable") from e
    except DatabaseError as e:
        logger.error(f"Database query error: {e}")
        raise DataRetrievalError(f"Error retrieving user data: {e}") from e

Custom Exception Hierarchy

# Custom exception hierarchy for a web application
class ApplicationError(Exception):
    """Base exception for all application errors."""
    
class ValidationError(ApplicationError):
    """Raised when input data fails validation."""
    
class AuthenticationError(ApplicationError):
    """Raised when authentication fails."""

class AuthorizationError(ApplicationError):
    """Raised when a user lacks permission for an action."""
    
class ResourceError(ApplicationError):
    """Base exception for resource-related errors."""
    
class ResourceNotFoundError(ResourceError):
    """Raised when a requested resource does not exist."""
    
class ResourceConflictError(ResourceError):
    """Raised when a resource operation would cause a conflict."""
    
class ServiceError(ApplicationError):
    """Base exception for service-related errors."""
    
class DatabaseError(ServiceError):
    """Raised when database operations fail."""
    
class ExternalServiceError(ServiceError):
    """Raised when external service calls fail."""

Context Managers for Resource Management

# Without context manager
def process_file(filename):
    file = open(filename, 'r')
    try:
        data = file.read()
        return process_data(data)
    finally:
        file.close()  # Easy to forget this

# With built-in context manager
def process_file(filename):
    with open(filename, 'r') as file:
        data = file.read()
        return process_data(data)  # File automatically closed

# Custom context manager for database transactions
class DatabaseTransaction:
    def __init__(self, connection):
        self.connection = connection
    
    def __enter__(self):
        self.cursor = self.connection.cursor()
        return self.cursor
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            # No exception occurred, commit the transaction
            self.connection.commit()
        else:
            # Exception occurred, rollback the transaction
            self.connection.rollback()
        self.cursor.close()
        # Returning False propagates any exceptions
        return False

# Using the custom context manager
def update_user_profile(user_id, profile_data):
    with DatabaseTransaction(get_db_connection()) as cursor:
        cursor.execute(
            "UPDATE users SET profile = %s WHERE id = %s",
            (json.dumps(profile_data), user_id)
        )
        # Transaction automatically committed on success
        # or rolled back on exception

Real-world Example: In a payment processing system, effective error handling is critical. Different types of errors (invalid payment details, insufficient funds, gateway timeouts) require different responses. A well-designed exception hierarchy allows the application to respond appropriately to each error type while maintaining a clean, maintainable codebase.

Testing and Test-Driven Development

Metaphor: Writing tests is like having a safety net when walking a tightrope. It doesn't prevent you from making mistakes, but it does prevent those mistakes from being catastrophic. As you gain confidence, the safety net allows you to move faster and take on more complex challenges.

Types of Tests

Writing Testable Code

# Hard to test
def process_payment(order_id):
    order = get_order_from_database(order_id)
    user = get_user_from_database(order.user_id)
    payment_gateway = PaymentGateway()
    result = payment_gateway.charge(
        user.credit_card,
        order.total,
        description=f"Order #{order.id}"
    )
    if result.success:
        update_order_status(order.id, "paid")
        send_confirmation_email(user.email, order)
    else:
        update_order_status(order.id, "payment_failed")
        send_failure_email(user.email, order, result.error)
    return result.success

# More testable
def process_payment(order, user, payment_gateway, email_sender):
    """
    Process payment for an order.
    
    Args:
        order (Order): The order to process
        user (User): The user who placed the order
        payment_gateway (PaymentGateway): Payment processor
        email_sender (EmailSender): Email service
        
    Returns:
        bool: True if payment successful, False otherwise
    """
    result = payment_gateway.charge(
        user.credit_card,
        order.total,
        description=f"Order #{order.id}"
    )
    
    if result.success:
        order.status = "paid"
        email_sender.send_confirmation(user.email, order)
    else:
        order.status = "payment_failed"
        email_sender.send_failure(user.email, order, result.error)
    
    return result.success

# Usage in production
def process_order_payment(order_id):
    order = get_order_from_database(order_id)
    user = get_user_from_database(order.user_id)
    return process_payment(
        order,
        user,
        PaymentGateway(),
        EmailService()
    )

# In tests
def test_process_payment_success():
    # Create test doubles
    order = MockOrder(id=1, total=100.00)
    user = MockUser(email="test@example.com", credit_card="4111111111111111")
    payment_gateway = MockPaymentGateway(should_succeed=True)
    email_sender = MockEmailSender()
    
    # Call function under test
    result = process_payment(order, user, payment_gateway, email_sender)
    
    # Assertions
    assert result is True
    assert order.status == "paid"
    assert email_sender.confirmation_sent_to == user.email
    assert not hasattr(email_sender, "failure_sent_to")

Test-Driven Development (TDD)

TDD follows a simple cycle:

  1. Red: Write a failing test
  2. Green: Write the simplest code to make the test pass
  3. Refactor: Improve the code while keeping tests passing
# Step 1: Red - Write a failing test
def test_calculate_total_with_tax():
    # Arrange
    items = [
        {"name": "Book", "price": 10.00, "taxable": True},
        {"name": "Food", "price": 20.00, "taxable": False}
    ]
    tax_rate = 0.08
    
    # Act
    total = calculate_total_with_tax(items, tax_rate)
    
    # Assert
    expected = 30.80  # Book price + tax + Food price
    assert total == expected

# Step 2: Green - Write the simplest code to make the test pass
def calculate_total_with_tax(items, tax_rate):
    total = 0
    for item in items:
        if item["taxable"]:
            total += item["price"] * (1 + tax_rate)
        else:
            total += item["price"]
    return total

# Step 3: Refactor - Improve the code while keeping tests passing
def calculate_total_with_tax(items, tax_rate):
    """
    Calculate total price including tax for applicable items.
    
    Args:
        items (list): List of item dictionaries with 'price' and 'taxable' keys
        tax_rate (float): Tax rate as a decimal (e.g., 0.08 for 8%)
    
    Returns:
        float: Total price including tax
    """
    def item_price_with_tax(item):
        """Calculate price for a single item, including tax if applicable."""
        price = item["price"]
        return price * (1 + tax_rate) if item["taxable"] else price
    
    return sum(item_price_with_tax(item) for item in items)

Real-world Impact: Companies that adopt test-driven development often report 40-80% fewer bugs in production. While TDD may slow down initial development, it dramatically reduces debugging and maintenance time. For web applications that need to remain stable over time, this trade-off is usually well worth it.

Performance Considerations

Analogy: Optimizing code performance is like tuning a race car. You want to find the right balance of speed, reliability, and maintainability. Sometimes a small adjustment can lead to significant improvements, but over-optimization can make the system brittle and hard to modify.

Common Performance Pitfalls

# Inefficient string concatenation in a loop
def build_report(items):
    result = ""
    for item in items:
        result = result + item.name + ": " + str(item.value) + "\n"
    return result

# Better: Using join or string interpolation
def build_report(items):
    lines = [f"{item.name}: {item.value}" for item in items]
    return "\n".join(lines)

# Inefficient list operations
def find_duplicates(items):
    duplicates = []
    for item in items:
        if items.count(item) > 1 and item not in duplicates:
            duplicates.append(item)
    return duplicates

# Better: Using sets for O(1) lookups
def find_duplicates(items):
    seen = set()
    duplicates = set()
    
    for item in items:
        if item in seen:
            duplicates.add(item)
        else:
            seen.add(item)
    
    return list(duplicates)

Memory Management

# Memory inefficient: Loading entire file into memory
def count_lines_with_string(filename, search_string):
    with open(filename, 'r') as f:
        content = f.read()  # Loads entire file into memory
    
    lines = content.split('\n')
    count = 0
    for line in lines:
        if search_string in line:
            count += 1
    
    return count

# Memory efficient: Processing one line at a time
def count_lines_with_string(filename, search_string):
    count = 0
    with open(filename, 'r') as f:
        for line in f:  # Iterates line by line
            if search_string in line:
                count += 1
    
    return count

# Using generators for memory efficiency
def process_large_dataset(filename):
    def parse_records(file):
        for line in file:
            # Yield each record instead of building a list
            yield parse_record(line)
    
    with open(filename, 'r') as f:
        # Process one record at a time without loading all into memory
        for record in parse_records(f):
            process_record(record)

Profiling and Optimization

import cProfile
import pstats

# Profile a function to identify bottlenecks
def profile_function(func, *args, **kwargs):
    profiler = cProfile.Profile()
    profiler.enable()
    
    result = func(*args, **kwargs)
    
    profiler.disable()
    stats = pstats.Stats(profiler).sort_stats('cumtime')
    stats.print_stats(20)  # Print top 20 time-consuming functions
    
    return result

# Example usage
profile_function(process_large_dataset, 'data.csv')

Performance Rule: "Premature optimization is the root of all evil" (Donald Knuth). First make your code correct and clear, then optimize if and where necessary based on profiling data.

Real-world Example: In a data visualization application, processing and rendering large datasets efficiently is critical for user experience. Techniques like pagination, lazy loading, streaming responses, and optimized algorithms can make the difference between a responsive application and one that times out and crashes.

Code Reviews and Quality Tools

Metaphor: Code reviews are like peer review in academic publishing. They ensure quality, catch issues the author might have missed, and spread knowledge throughout the team. Automated quality tools are like spell-checkers—they catch obvious issues so human reviewers can focus on deeper concerns.

Effective Code Reviews

Code Review Checklist

Automated Quality Tools

Linters

Tools that check code for potential errors, bugs, and style issues:

# Installing and using flake8
pip install flake8

# Running flake8 on a file
flake8 my_module.py

# Running flake8 on a directory
flake8 my_project/

# Configuration in setup.cfg
# [flake8]
# max-line-length = 88
# exclude = .git,__pycache__,build,dist
# ignore = E203,W503

Formatters

Tools that automatically format code according to style rules:

# Installing and using black
pip install black

# Formatting a file
black my_module.py

# Formatting a directory
black my_project/

# Checking if files would be reformatted
black --check my_project/

Type Checkers

Tools that perform static type checking:

# Using type annotations and mypy
from typing import List, Dict, Optional

def process_user_data(user_id: int, fields: List[str]) -> Dict[str, Optional[str]]:
    """Process user data for specified fields."""
    user = get_user(user_id)
    result = {}
    
    for field in fields:
        result[field] = getattr(user, field, None)
    
    return result

# Running mypy
mypy my_module.py

Security Scanners

Tools that check for security vulnerabilities:

# Installing and using bandit
pip install bandit

# Scanning a file
bandit my_module.py

# Scanning a directory recursively
bandit -r my_project/

Real-world Integration: In professional development environments, these tools are typically integrated into a Continuous Integration (CI) pipeline. For example, GitHub Actions or Jenkins can run linters, formatters, type checkers, and security scanners automatically on every pull request, ensuring code quality standards are maintained across the codebase.

Practical Application: Refactoring Bad Code

Let's apply what we've learned by refactoring a poorly written function into a high-quality implementation:

Original Code (What Not To Do)

def p(d, id, t, s=None):
    # get user
    c.execute("SELECT * FROM users WHERE id = " + str(id))
    u = c.fetchone()
    if not u:
        return 0
    # check type
    if t == "post":
        if s:
            if s == "draft":
                q = "INSERT INTO posts (user_id, title, content, created_at, status) VALUES (%s, %s, %s, %s, %s)"
                c.execute(q, (id, d["title"], d["content"], datetime.now(), "draft"))
                db.commit()
                return c.lastrowid
            elif s == "publish":
                q = "INSERT INTO posts (user_id, title, content, created_at, status) VALUES (%s, %s, %s, %s, %s)"
                c.execute(q, (id, d["title"], d["content"], datetime.now(), "published"))
                db.commit()
                return c.lastrowid
            else:
                return 0
        else:
            q = "INSERT INTO posts (user_id, title, content, created_at, status) VALUES (%s, %s, %s, %s, %s)"
            c.execute(q, (id, d["title"], d["content"], datetime.now(), "published"))
            db.commit()
            return c.lastrowid
    elif t == "comment":
        pid = d["post_id"]
        c.execute("SELECT * FROM posts WHERE id = " + str(pid))
        p = c.fetchone()
        if not p:
            return 0
        q = "INSERT INTO comments (user_id, post_id, content, created_at) VALUES (%s, %s, %s, %s)"
        c.execute(q, (id, pid, d["content"], datetime.now()))
        db.commit()
        return c.lastrowid
    else:
        return 0

Refactored Code

from enum import Enum
from datetime import datetime
from typing import Dict, Optional, Union, Any


class ContentType(Enum):
    """Types of content that can be created."""
    POST = "post"
    COMMENT = "comment"


class PostStatus(Enum):
    """Possible status values for posts."""
    DRAFT = "draft"
    PUBLISHED = "published"


class DatabaseError(Exception):
    """Base exception for database-related errors."""
    pass


class UserNotFoundError(DatabaseError):
    """Raised when a requested user does not exist."""
    pass


class PostNotFoundError(DatabaseError):
    """Raised when a requested post does not exist."""
    pass


class ValidationError(Exception):
    """Raised when input data fails validation."""
    pass


def get_user(cursor, user_id: int) -> Dict[str, Any]:
    """
    Retrieve a user from the database by ID.
    
    Args:
        cursor: Database cursor
        user_id: User ID to retrieve
        
    Returns:
        Dictionary containing user data
        
    Raises:
        UserNotFoundError: If user does not exist
    """
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    user = cursor.fetchone()
    
    if not user:
        raise UserNotFoundError(f"User with ID {user_id} not found")
    
    return user


def get_post(cursor, post_id: int) -> Dict[str, Any]:
    """
    Retrieve a post from the database by ID.
    
    Args:
        cursor: Database cursor
        post_id: Post ID to retrieve
        
    Returns:
        Dictionary containing post data
        
    Raises:
        PostNotFoundError: If post does not exist
    """
    cursor.execute("SELECT * FROM posts WHERE id = %s", (post_id,))
    post = cursor.fetchone()
    
    if not post:
        raise PostNotFoundError(f"Post with ID {post_id} not found")
    
    return post


def create_post(
    cursor,
    connection,
    user_id: int,
    data: Dict[str, str],
    status: PostStatus = PostStatus.PUBLISHED
) -> int:
    """
    Create a new post.
    
    Args:
        cursor: Database cursor
        connection: Database connection
        user_id: ID of the user creating the post
        data: Dictionary containing post data (title, content)
        status: Status for the new post (draft or published)
        
    Returns:
        ID of the newly created post
        
    Raises:
        ValidationError: If required data is missing
    """
    # Validate required fields
    if "title" not in data or not data["title"]:
        raise ValidationError("Post title is required")
    
    if "content" not in data or not data["content"]:
        raise ValidationError("Post content is required")
    
    # Insert post
    query = """
        INSERT INTO posts (user_id, title, content, created_at, status)
        VALUES (%s, %s, %s, %s, %s)
    """
    cursor.execute(
        query,
        (user_id, data["title"], data["content"], datetime.now(), status.value)
    )
    connection.commit()
    
    return cursor.lastrowid


def create_comment(
    cursor,
    connection,
    user_id: int,
    data: Dict[str, Any]
) -> int:
    """
    Create a new comment on a post.
    
    Args:
        cursor: Database cursor
        connection: Database connection
        user_id: ID of the user creating the comment
        data: Dictionary containing comment data (post_id, content)
        
    Returns:
        ID of the newly created comment
        
    Raises:
        ValidationError: If required data is missing
        PostNotFoundError: If the referenced post does not exist
    """
    # Validate required fields
    if "post_id" not in data:
        raise ValidationError("Post ID is required")
    
    if "content" not in data or not data["content"]:
        raise ValidationError("Comment content is required")
    
    # Verify post exists
    post_id = data["post_id"]
    get_post(cursor, post_id)  # Will raise PostNotFoundError if not found
    
    # Insert comment
    query = """
        INSERT INTO comments (user_id, post_id, content, created_at)
        VALUES (%s, %s, %s, %s)
    """
    cursor.execute(
        query,
        (user_id, post_id, data["content"], datetime.now())
    )
    connection.commit()
    
    return cursor.lastrowid


def create_content(
    cursor,
    connection,
    data: Dict[str, Any],
    user_id: int,
    content_type: ContentType,
    status: Optional[PostStatus] = None
) -> int:
    """
    Create content (post or comment) in the database.
    
    Args:
        cursor: Database cursor
        connection: Database connection
        data: Dictionary containing content data
        user_id: ID of the user creating the content
        content_type: Type of content (post or comment)
        status: Status for posts (draft or published)
        
    Returns:
        ID of the newly created content
        
    Raises:
        UserNotFoundError: If user does not exist
        ValidationError: If content type is invalid or required data is missing
        PostNotFoundError: If a referenced post does not exist
    """
    # Verify user exists
    get_user(cursor, user_id)  # Will raise UserNotFoundError if not found
    
    if content_type == ContentType.POST:
        post_status = status or PostStatus.PUBLISHED
        return create_post(cursor, connection, user_id, data, post_status)
    elif content_type == ContentType.COMMENT:
        return create_comment(cursor, connection, user_id, data)
    else:
        raise ValidationError(f"Invalid content type: {content_type}")


# Example usage:
def example_usage():
    try:
        # Create a post
        post_id = create_content(
            cursor,
            db_connection,
            {"title": "Hello World", "content": "This is my first post"},
            user_id=42,
            content_type=ContentType.POST,
            status=PostStatus.DRAFT
        )
        print(f"Created post with ID: {post_id}")
        
        # Create a comment
        comment_id = create_content(
            cursor,
            db_connection,
            {"post_id": post_id, "content": "Great post!"},
            user_id=42,
            content_type=ContentType.COMMENT
        )
        print(f"Created comment with ID: {comment_id}")
        
    except UserNotFoundError as e:
        print(f"Error: {e}")
    except PostNotFoundError as e:
        print(f"Error: {e}")
    except ValidationError as e:
        print(f"Validation error: {e}")
    except DatabaseError as e:
        print(f"Database error: {e}")
        db_connection.rollback()

Improvements Made

Real-world Impact: The refactored code is not just more readable—it's also more robust, secure, and maintainable. In a professional environment, these qualities directly translate to fewer bugs, faster feature development, and easier onboarding for new team members.

Conclusion

Today, we've explored the craft of writing high-quality Python code. The practices we've covered—from PEP 8 style guidelines to effective error handling, from testable function design to code organization principles—are essential tools in your development toolkit.

Remember that code quality isn't about adhering to arbitrary rules; it's about writing code that effectively communicates your intent to both computers and human readers. As the aphorism goes, "Code is read much more often than it is written."

As we move into web development in the coming weeks, these principles become even more important. Web applications are typically larger, more complex, and longer-lived than simple scripts. They often involve multiple developers working together over extended periods. High-quality code provides the foundation that makes such collaboration possible and productive.

Continue to practice these principles in all your coding. Think of them not as constraints but as liberating patterns that free you to focus on solving the interesting problems rather than debugging poor implementations. The time you invest in mastering these practices will pay dividends throughout your career.

Additional Resources