Document Python Code Effectively

Learn how to write clear docstrings, comments, and type hints that make your Python code understandable to other developers and your future self.

7 min read

Documenting Python code means writing explanations that help other developers, and your future self, understand what the code does and why it was written that way. Python provides three built-in tools for this: docstrings, comments, and type hints. Each serves a different purpose.

Good documentation lives close to the code. When you change a function's behavior, you update its docstring in the same commit. Outdated documentation is worse than no documentation because it actively misleads the reader.

The habits in this article build on the formatting rules from the PEP 8 style guide. Here is a well-formed function docstring following the standard conventions:

pythonpython
def connect_to_database(host, port, timeout=30):
    """Open a connection to the database server.
 
    The connection uses TCP and supports automatic reconnection if the
    server becomes unreachable during the session. Callers should close
    the connection with the disconnect function when finished.
    """
    connection = _create_socket(host, port)
    connection.settimeout(timeout)
    return connection

For simple functions where the name already explains the purpose, a one-line docstring on the same line is acceptable. Reserve the multi-line format for functions where the behavior needs more context than the name and signature already provide:

pythonpython
def is_valid_email(address):
    """Return True if address contains an @ sign and a domain part."""
    return "@" in address and "." in address.split("@")[-1]

Class docstrings describe the class purpose and public behavior. Method docstrings follow the same rules as function docstrings. Module docstrings go at the top of the file and describe what the module provides.

Triple double-quotes are the convention, even for one-liners. Tools like Sphinx and pydoc expect them, and consistency across a codebase matters more than personal preference.

Comments: explain the reasoning, not the action

Comments start with # and are ignored by the interpreter. Their job is to explain decisions that are not obvious from the code itself. A comment should answer "why," not "what."

The worst comment restates the code. The best comment explains a constraint, a workaround, or a deliberate choice that the reader would otherwise question:

pythonpython
# Bad: restates what is already obvious
count = count + 1  # increment count
 
# Good: explains a non-obvious decision
count = count + 1  # compensate for the 0-based index in the external API

Block comments go on their own line before the code they describe. Inline comments go on the same line and should be used sparingly. If a block of code needs an inline comment on every line, the code is probably too clever and should be rewritten for clarity.

Avoid commenting out old code and leaving it in the file. Version control remembers the history. Dead code creates noise and makes the reader wonder whether it was left intentionally.

pythonpython
def process_orders(orders):
    # Only process paid orders that have at least one item.
    # Unpaid orders are handled by the billing cron job.
    valid = [o for o in orders if o.is_paid and o.items]
 
    # Sort by priority (high to low) so urgent orders ship first.
    valid.sort(key=lambda o: o.priority, reverse=True)
 
    for order in valid:
        ship(order)

The function name and variable names already say what happens. The comments explain the filtering rule and the sorting rationale, which are the non-obvious decisions.

Type hints: document the expected types

Type hints were added in Python 3.5 and have become a standard way to document what types a function expects and returns. They are optional annotations that tools like mypy and your editor can check, but Python does not enforce them at runtime.

A function with type hints says more about its contract than one without. The reader knows at a glance what type each parameter should be and what type the function returns:

pythonpython
from datetime import datetime
 
def calculate_age(birth_date: datetime, reference_date: datetime | None = None) -> int:
    """Return the age in whole years between birth_date and reference_date.
 
    If reference_date is not provided, the current date is used.
    """
    if reference_date is None:
        reference_date = datetime.now()
    return reference_date.year - birth_date.year

For collections, use the generic types from the typing module. These tell the reader what the collection contains, not just that it is a list or dict:

pythonpython
from typing import Sequence
 
def find_active_users(users: Sequence[dict]) -> list[str]:
    """Return the names of users whose active field is truthy."""
    return [u["name"] for u in users if u.get("active")]

Type hints are documentation first, static checking second. Even if you never run mypy, the annotations help readers understand the expected inputs and outputs without reading the implementation. For more on type hints, see the article on advanced Python type hints with generics.

Module-level docstrings

Every Python file that is meant to be imported should start with a module docstring. It describes what the module provides and how to use it. The docstring goes before any imports:

pythonpython
"""Utility functions for validating and normalizing user input.
 
This module provides validation functions for common input types like
email addresses, phone numbers, and URLs. Each function returns a
boolean and does not raise exceptions for invalid input.
 
Example:
    from myapp.validation import is_valid_email
    if not is_valid_email(user_input):
        raise ValueError("Invalid email address")
"""
 
import re
 
 
EMAIL_PATTERN: re.Pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
 
 
def is_valid_email(address: str) -> bool:
    """Return True if address matches the standard email pattern."""
    return bool(EMAIL_PATTERN.match(address))

The example section shows a quick usage pattern. Many documentation generators render this as the first thing a user sees when they look up the module.

Documenting classes

A class docstring describes what the class represents and its main public interface. List the most important methods by name and briefly describe their purpose. Do not repeat the method docstrings:

pythonpython
class ShoppingCart:
    """A mutable collection of items with quantity tracking and total calculation.
 
    Public methods:
        add_item: Add a product with an optional quantity.
        remove_item: Remove a product from the cart.
        total: Return the total price of all items.
        clear: Remove all items from the cart.
    """
 
    def __init__(self):
        self._items: dict[str, int] = {}
 
    def add_item(self, product_id: str, quantity: int = 1) -> None:
        """Add quantity units of product_id to the cart."""
        self._items[product_id] = self._items.get(product_id, 0) + quantity

Documentation-driven habits

Write the docstring before you write the function body. This forces you to clarify what the function should do before you get lost in how to do it. If you cannot write a clear one-line summary, the function's purpose is probably unclear and needs more thought.

Update the docstring when you change the function. A docstring that says "returns a list" when the function now returns a generator is actively harmful. Treat the docstring as part of the function's contract, not an afterthought.

Use the help() function to verify your docstrings during development. If help(your_function) does not give you enough information to use the function correctly, the docstring needs more detail.

For more on writing maintainable code, see Python functions explained.

Rune AI

Rune AI

Key Insights

  • Use triple-quoted docstrings for modules, classes, and public functions.
  • Follow PEP 257: one-line summary, blank line, then detailed description.
  • Write comments that explain why, not what the code already says.
  • Use type hints to document function signatures and variable types.
  • Keep documentation close to the code it describes; outdated docs are worse than no docs.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a comment and a docstring?

A comment starts with # and is ignored by the Python interpreter. A docstring is a triple-quoted string placed as the first statement in a module, class, or function. Docstrings are stored in the __doc__ attribute and are accessible at runtime by tools like help().

Should every function have a docstring?

Every public function, class, and module should have a docstring. Small private helper functions with obvious names may not need one, but when in doubt, write a one-line docstring. The cost of writing it is seconds; the cost of not having it is minutes of confusion later.

Conclusion

Good documentation is not about writing more words. It is about writing the right words in the right places so the next developer, including your future self, can understand the code without tracing through every line.