Skip to main content
โšก Calmops

Python Comments, Docstrings, and Documentation: Best Practices

Introduction

Imagine opening a Python file you wrote six months ago. You stare at the code, trying to remember why you made certain decisions. What does this function do? Why is this variable named that way? If only you’d documented it better.

Documentation is often overlooked in programming education, yet it’s one of the most valuable skills you can develop. Good documentation makes code easier to understand, maintain, and collaborate on. It transforms code from something only you understand into something your entire team can work with confidently.

In this guide, we’ll explore Python’s documentation tools: comments, docstrings, and documentation practices. You’ll learn when to use each, how to write them effectively, and how to follow Python community standards.


Why Documentation Matters

The Cost of Poor Documentation

  • Maintenance burden - Developers waste time understanding code
  • Bugs and errors - Misunderstanding code leads to mistakes
  • Collaboration friction - Team members can’t work efficiently together
  • Knowledge loss - When developers leave, their understanding leaves with them
  • Technical debt - Undocumented code becomes harder to refactor

The Benefits of Good Documentation

โœ… Clarity - Code intent is immediately obvious
โœ… Maintainability - Easier to modify and extend code
โœ… Collaboration - Teams work more efficiently
โœ… Professionalism - Well-documented code is a sign of quality
โœ… Knowledge preservation - Understanding persists beyond individual developers


Comments: Explaining the “Why”

What Are Comments?

Comments are lines of text that explain code but aren’t executed. They start with the # symbol and are ignored by Python.

# This is a comment
x = 5  # This is an inline comment

Single-Line Comments

Single-line comments explain a specific line or small block of code:

# Calculate the total price including tax
total = price * (1 + tax_rate)

# Check if user has admin privileges
if user.role == "admin":
    grant_access()

Multi-Line Comments

For longer explanations, use multiple comment lines:

# This algorithm uses a greedy approach to find the shortest path.
# It iterates through all nodes, selecting the nearest unvisited node
# at each step. While not optimal for all cases, it's efficient for
# most practical applications.
def find_shortest_path(graph, start):
    pass

When to Use Comments

Use comments to explain:

  • The “why” - Why you made a decision, not what the code does
  • Complex logic - Non-obvious algorithms or calculations
  • Workarounds - Temporary fixes or hacks with explanations
  • Important notes - Warnings, gotchas, or assumptions
  • TODO items - Future improvements or known issues
# Good: Explains why
# We use a set here for O(1) lookup time instead of a list
seen = set()

# Bad: Explains what (code already shows this)
# Add item to the set
seen.add(item)

# Good: Explains a workaround
# TODO: Replace with proper error handling once API is updated
try:
    result = api.fetch_data()
except TimeoutError:
    result = cache.get_last_result()  # Temporary fallback

# Good: Warns about important behavior
# WARNING: This function modifies the input list in place
def sort_in_place(items):
    items.sort()

Comment Anti-Patterns

Avoid these common mistakes:

# Bad: Obvious comments that add no value
x = 5  # Set x to 5
y = x + 1  # Add 1 to x and assign to y

# Bad: Outdated comments that contradict code
# This function returns a string
def get_count():
    return 42  # Returns an integer, not a string!

# Bad: Commented-out code
# def old_function():
#     return "old"
# Use version control instead of leaving dead code

# Bad: Excessive comments
# Loop through each item
for item in items:
    # Check if item is valid
    if is_valid(item):
        # Process the item
        process(item)

Docstrings: Documenting Code Objects

What Are Docstrings?

Docstrings are string literals that document Python objects: modules, classes, functions, and methods. They’re the first statement after the definition and are accessible via the __doc__ attribute.

def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}!"

# Access the docstring
print(greet.__doc__)  # Output: Say hello to someone.

Single-Line Docstrings

For simple functions, use a single-line docstring:

def add(x, y):
    """Return the sum of two numbers."""
    return x + y

def is_valid_email(email):
    """Check if an email address is valid."""
    return "@" in email and "." in email

Multi-Line Docstrings

For complex functions, use multi-line docstrings with a summary, blank line, and detailed description:

def calculate_compound_interest(principal, rate, time, compounds_per_year):
    """Calculate compound interest.
    
    This function computes the final amount after applying compound interest
    to an initial principal over a specified time period.
    
    Args:
        principal: Initial amount in dollars
        rate: Annual interest rate as a decimal (e.g., 0.05 for 5%)
        time: Time period in years
        compounds_per_year: Number of times interest is compounded per year
    
    Returns:
        Final amount after compound interest
    
    Raises:
        ValueError: If principal or rate is negative
    """
    if principal < 0 or rate < 0:
        raise ValueError("Principal and rate must be non-negative")
    
    return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)

Module Docstrings

Document the purpose of a module at the top of the file:

"""User management module.

This module provides classes and functions for managing user accounts,
including authentication, profile management, and permission handling.

Classes:
    User: Represents a user account
    UserManager: Manages user operations

Functions:
    authenticate: Verify user credentials
    create_user: Create a new user account
"""

import hashlib
from datetime import datetime

class User:
    """Represents a user account."""
    pass

Class Docstrings

Document classes with their purpose and key attributes:

class BankAccount:
    """Represents a bank account.
    
    This class manages account operations including deposits, withdrawals,
    and balance inquiries.
    
    Attributes:
        account_number: Unique identifier for the account
        owner: Name of the account owner
        balance: Current account balance in dollars
    """
    
    def __init__(self, account_number, owner, initial_balance=0):
        """Initialize a bank account.
        
        Args:
            account_number: Unique account identifier
            owner: Name of the account owner
            initial_balance: Starting balance (default: 0)
        """
        self.account_number = account_number
        self.owner = owner
        self.balance = initial_balance
    
    def deposit(self, amount):
        """Deposit money into the account.
        
        Args:
            amount: Amount to deposit in dollars
        
        Raises:
            ValueError: If amount is negative or zero
        """
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.balance += amount

Docstring Formats

Python supports several docstring formats. Here are the most common:

Google Style

def fetch_user_data(user_id, include_profile=False):
    """Fetch user data from the database.
    
    Args:
        user_id: The unique user identifier
        include_profile: Whether to include profile information (default: False)
    
    Returns:
        A dictionary containing user data
    
    Raises:
        UserNotFoundError: If user doesn't exist
        DatabaseError: If database connection fails
    """
    pass

NumPy Style

def fetch_user_data(user_id, include_profile=False):
    """Fetch user data from the database.
    
    Parameters
    ----------
    user_id : int
        The unique user identifier
    include_profile : bool, optional
        Whether to include profile information (default: False)
    
    Returns
    -------
    dict
        A dictionary containing user data
    
    Raises
    ------
    UserNotFoundError
        If user doesn't exist
    DatabaseError
        If database connection fails
    """
    pass

reStructuredText (Sphinx)

def fetch_user_data(user_id, include_profile=False):
    """Fetch user data from the database.
    
    :param user_id: The unique user identifier
    :type user_id: int
    :param include_profile: Whether to include profile information
    :type include_profile: bool
    :return: A dictionary containing user data
    :rtype: dict
    :raises UserNotFoundError: If user doesn't exist
    :raises DatabaseError: If database connection fails
    """
    pass

The __doc__ Attribute

Docstrings are stored in the __doc__ attribute and can be accessed programmatically:

def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}!"

# Access docstring
print(greet.__doc__)  # Output: Say hello to someone.

# Check if docstring exists
if greet.__doc__:
    print("Function is documented")

# Use in help system
help(greet)
# Output:
# Help on function greet in module __main__:
# 
# greet(name)
#     Say hello to someone.

Tools That Leverage Documentation

The help() Function

Python’s built-in help() function displays docstrings:

help(print)  # Shows documentation for print function
help(list)   # Shows documentation for list class

pydoc Module

Generate HTML documentation from docstrings:

# View documentation in terminal
pydoc mymodule

# Generate HTML documentation
pydoc -w mymodule

Sphinx

Professional documentation generator:

# Install Sphinx
pip install sphinx

# Create documentation project
sphinx-quickstart docs/

IDE Integration

Most IDEs display docstrings in tooltips and autocomplete:

def calculate(x, y):
    """Calculate the sum of two numbers."""
    return x + y

# When you type calculate( in your IDE,
# it shows: "Calculate the sum of two numbers."

PEP 8 and PEP 257 Standards

PEP 8: Style Guide

PEP 8 provides general guidelines for Python code style, including comments:

  • Use complete sentences for comments
  • Comments should be in English
  • Inline comments should be separated by at least two spaces
  • Comments should not state the obvious

PEP 257: Docstring Conventions

PEP 257 provides specific guidelines for docstrings:

  • Use triple double quotes (""") for all docstrings
  • One-line docstrings fit on one line
  • Multi-line docstrings have a summary line, blank line, then details
  • Docstrings should describe what the function does, not how
  • Use imperative mood: “Return” not “Returns”
# Good: Follows PEP 257
def calculate_total(items):
    """Calculate the total price of items.
    
    Args:
        items: List of item dictionaries with 'price' key
    
    Returns:
        Total price as a float
    """
    return sum(item["price"] for item in items)

# Bad: Violates PEP 257
def calculate_total(items):
    """
    This function calculates the total price of items by iterating
    through the list and summing up all the prices.
    """
    return sum(item["price"] for item in items)

Best Practices for Documentation

1. Document the “Why,” Not the “What”

# Good: Explains why
# We use a dictionary for O(1) lookup instead of a list
user_cache = {}

# Bad: Explains what (code already shows this)
# Create an empty dictionary
user_cache = {}

2. Keep Documentation Close to Code

# Good: Documentation near the code it describes
def process_data(data):
    """Process raw data and return cleaned results."""
    # Remove duplicates first for efficiency
    unique_data = list(set(data))
    return [clean(item) for item in unique_data]

# Bad: Documentation far from relevant code
def process_data(data):
    # This function processes data
    unique_data = list(set(data))
    # Remove duplicates
    return [clean(item) for item in unique_data]

3. Update Documentation When Code Changes

# Bad: Outdated documentation
def get_user_count():
    """Return the number of active users."""
    # Now returns all users, not just active ones
    return len(users)

# Good: Updated documentation
def get_user_count():
    """Return the total number of users."""
    return len(users)

4. Balance Documentation

# Bad: Over-documented
def add(x, y):
    """Add two numbers together.
    
    This function takes two numeric parameters and returns their sum.
    It uses the + operator to perform the addition operation.
    The result is returned to the caller.
    
    Args:
        x: First number to add
        y: Second number to add
    
    Returns:
        The sum of x and y
    """
    return x + y

# Good: Appropriately documented
def add(x, y):
    """Return the sum of two numbers."""
    return x + y

# Bad: Under-documented
def process_user_data(data):
    result = []
    for item in data:
        if item["status"] == "active":
            result.append(transform(item))
    return result

# Good: Appropriately documented
def process_user_data(data):
    """Filter active users and apply transformation.
    
    Args:
        data: List of user dictionaries
    
    Returns:
        List of transformed active users
    """
    result = []
    for item in data:
        if item["status"] == "active":
            result.append(transform(item))
    return result

5. Use Type Hints with Docstrings

# Good: Type hints + docstring
def calculate_discount(price: float, discount_percent: float) -> float:
    """Calculate the discounted price.
    
    Args:
        price: Original price in dollars
        discount_percent: Discount percentage (0-100)
    
    Returns:
        Discounted price
    """
    return price * (1 - discount_percent / 100)

6. Document Exceptions

def divide(x: float, y: float) -> float:
    """Divide x by y.
    
    Args:
        x: Numerator
        y: Denominator
    
    Returns:
        Result of x / y
    
    Raises:
        ZeroDivisionError: If y is zero
    """
    if y == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return x / y

Common Documentation Anti-Patterns

Anti-Pattern 1: Obvious Comments

# Bad: Obvious comment
i = 0  # Set i to 0
i += 1  # Increment i

# Good: No comment needed (code is self-explanatory)
counter = 0
counter += 1

Anti-Pattern 2: Misleading Documentation

# Bad: Documentation doesn't match code
def get_user_count():
    """Return the number of active users."""
    return len(all_users)  # Returns all users, not just active

# Good: Accurate documentation
def get_user_count():
    """Return the total number of users."""
    return len(all_users)

Anti-Pattern 3: Commented-Out Code

# Bad: Dead code clutters the file
# def old_function():
#     return "old"
# 
# def another_old_function():
#     pass

# Good: Use version control instead
# (Remove dead code and rely on git history)

Anti-Pattern 4: Excessive Documentation

# Bad: Over-documented simple code
def is_positive(x):
    """Check if a number is positive.
    
    This function takes a numeric value and determines whether it is
    greater than zero. If the value is greater than zero, it returns
    True. Otherwise, it returns False.
    
    Args:
        x: A numeric value to check
    
    Returns:
        True if x > 0, False otherwise
    """
    return x > 0

# Good: Appropriately documented
def is_positive(x):
    """Return True if x is positive."""
    return x > 0

Practical Example: Well-Documented Code

"""User authentication module.

This module provides authentication and authorization functionality
for user accounts, including login, logout, and permission checking.
"""

from datetime import datetime, timedelta
from typing import Optional


class AuthenticationError(Exception):
    """Raised when authentication fails."""
    pass


class User:
    """Represents a user account.
    
    Attributes:
        username: Unique username
        email: User's email address
        is_active: Whether the account is active
    """
    
    def __init__(self, username: str, email: str):
        """Initialize a user account.
        
        Args:
            username: Unique username
            email: User's email address
        """
        self.username = username
        self.email = email
        self.is_active = True
        self.last_login: Optional[datetime] = None
    
    def login(self) -> None:
        """Record a successful login.
        
        Raises:
            AuthenticationError: If account is not active
        """
        if not self.is_active:
            raise AuthenticationError("Account is not active")
        self.last_login = datetime.now()
    
    def has_permission(self, permission: str) -> bool:
        """Check if user has a specific permission.
        
        Args:
            permission: Permission to check
        
        Returns:
            True if user has permission, False otherwise
        """
        # TODO: Implement permission system
        return True


def authenticate_user(username: str, password: str) -> User:
    """Authenticate a user with username and password.
    
    Args:
        username: User's username
        password: User's password
    
    Returns:
        Authenticated User object
    
    Raises:
        AuthenticationError: If credentials are invalid
    """
    # TODO: Implement actual authentication logic
    user = User(username, "[email protected]")
    user.login()
    return user

Conclusion

Documentation is not an afterthoughtโ€”it’s a fundamental part of professional programming. By mastering comments, docstrings, and documentation practices, you write code that’s:

Key takeaways:

  1. Comments explain the “why” - Use them for non-obvious logic and decisions
  2. Docstrings document objects - Use them for modules, classes, functions, and methods
  3. Follow PEP 257 - Use standard docstring conventions
  4. Balance is key - Document enough to be clear, not so much that it’s noise
  5. Keep documentation current - Update it when code changes
  6. Use tools - Leverage help(), pydoc, and Sphinx for documentation generation
  7. Be consistent - Choose a docstring format and stick with it

Well-documented code is a sign of professionalism and respect for your teammates and future self. Start applying these practices in your next project, and you’ll see immediate improvements in code clarity and maintainability.

Happy documenting! ๐Ÿ๐Ÿ“š


Quick Reference

# Single-line comment
# Explains the why, not the what

# Multi-line comment
# Use for longer explanations
# that span multiple lines

def function():
    """Single-line docstring for simple functions."""
    pass

def complex_function(param1, param2):
    """Summary line.
    
    Longer description explaining the function's purpose,
    behavior, and any important details.
    
    Args:
        param1: Description of param1
        param2: Description of param2
    
    Returns:
        Description of return value
    
    Raises:
        ExceptionType: When this exception is raised
    """
    pass

class MyClass:
    """Class docstring explaining purpose and attributes."""
    
    def __init__(self):
        """Initialize the class."""
        pass

Comments