Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Readable Python Docstrings

Learn to write readable docstrings and comments in Python. This lesson covers best practices for documenting code with clear, concise docstrings and helpful comments. Includes hands-on exercises, common pitfalls, and next steps for progress.

Focus: write readable docstrings and comments

Sponsored

Have you ever opened a piece of your own code from months ago and spent five minutes deciphering what a function does? Or worse, inherited a colleague's function with no explanation and a single cryptic variable name? That friction is the exact problem this lesson solves. Writing readable docstrings and comments isn't about being verbose — it's about making your code self-documenting so that future you (and your teammates) can understand intent, usage, and edge cases without reading every line. Let's turn your Python code into a clear, maintainable narrative.

The Problem This Lesson Solves

Every Python developer has faced the cost of unclear code: - Wasted debugging time: You spend 30 minutes tracing a function because you can't recall its parameters or side effects. - Onboarding friction: New team members must reverse-engineer each function's purpose. - Stale or missing documentation: Separate documentation files often get outdated; docstrings live with the code and stay in sync. - Tooling blind spots: Tools like help() or IDE autocompletion only show docstrings — if they're missing, you lose a major productivity boost.

Without proper docstrings and comments, your code is a black box. This lesson equips you with a consistent, readable style so that every function, class, and module communicates its purpose clearly.

Core Concept / Mental Model

Think of docstrings as the table of contents for your code — they tell a reader what a module, class, or function does, what it expects, and what it returns. Comments are the margin notes — they explain why a particular implementation choice was made when the code itself isn't obvious.

  • Docstrings are for users of your code (including yourself tomorrow).
  • Comments are for maintainers (including you during a bug fix).

The Python Docstring Convention

Python strongly encourages adding a string literal as the first statement in a module, function, class, or method. This string is stored in the .__doc__ attribute and is accessible via help(). The official PEP 257 defines the convention.

  • One-liner docstrings: For simple functions, keep the summary and what's returned on one line.
  • Multi-line docstrings: For complex functions, follow this structure: 1. Summary line (one sentence, imperative mood: "Calculate", "Return", "Find"). 2. Blank line. 3. Extended description (optional, if more detail is needed). 4. Arguments, Returns, Raises sections (commonly in Google, NumPy/SciPy, or Sphinx style).

Comment Philosophy

  • Explain why, not what. The code itself shows what it does. A comment should explain the reasoning behind a non-obvious decision.
  • Avoid obvious comments. # Increment counter by 1 next to i += 1 adds noise, not value.
  • Use comments to clarify tricky logic, workarounds, or assumptions.

How It Works Step by Step

Here's the step-by-step process to add readability to your code.

Step 1: Write a Docstring for Every Public Function

Start with a one-line summary. If the function is more complex, expand to multi-line.

def calculate_area(radius: float) -> float:
    """Return the area of a circle given its radius."""
    return 3.14159 * radius ** 2

Step 2: Add Parameter and Return Documentation

Use a consistent style. The Google style is popular for its readability.

def fetch_user_data(user_id: int, include_inactive: bool = False) -> dict | None:
    """
    Retrieve user data from the database.

    Args:
        user_id: The unique identifier for the user.
        include_inactive: If True, include deactivated users in the result.

    Returns:
        A dictionary with user information if found, or None if not found.

    Raises:
        ValueError: If user_id is negative.
    """
    if user_id < 0:
        raise ValueError("user_id must be non-negative")
    # ... database query logic ...
    return {"id": user_id, "active": True}  # simplified

Step 3: Write Comments for Non-Obvious Decisions

If you had to choose a particular algorithm or work around a bug, leave a comment.

def process_payment(amount: float) -> bool:
    # Using decimal instead of float to avoid floating-point rounding errors
    from decimal import Decimal
    amount_decimal = Decimal(str(amount))
    # ... payment gateway logic ...

Step 4: Keep Docstrings Fresh

When you change a function's behavior, update its docstring immediately. Stale docstrings are worse than none at all.

Step 5: Use Type Hints as Complementary Documentation

Type hints (e.g., radius: float, -> float) reduce the need to explain types in the docstring, but don't eliminate the need for a summary.

Hands-on Walkthrough

Let's practice by enhancing a poorly documented module.

Example 1: Bad vs. Good Docstring

Bad (no docstring):

def calc(x, y):
    return (x * y) + (x ** 2)

What does this do? Only the reader's guesswork.

Good:

def calc_future_value(principal: float, rate: float) -> float:
    """
    Compute the future value of an investment after one period.

    The formula is: principal * rate + principal ** 2 (a simplified model).

    Args:
        principal: Initial amount invested.
        rate: Annual interest rate as a decimal (e.g., 0.05 for 5%).

    Returns:
        The estimated future value after one period.
    """
    return (principal * rate) + (principal ** 2)

Output from help(calc_future_value):

calc_future_value(principal: float, rate: float) -> float
    Compute the future value of an investment after one period.

    The formula is: principal * rate + principal ** 2 (a simplified model).

    Args:
        principal: Initial amount invested.
        rate: Annual interest rate as a decimal (e.g., 0.05 for 5%).

    Returns:
        The estimated future value after one period.

Example 2: Comment for a Tricky Workaround

def upload_file(file_path: str) -> bool:
    # Using try-except instead of checking file existence first to avoid
    # TOCTOU (time-of-check to time-of-use) race condition.
    try:
        with open(file_path, 'rb') as f:
            # ... upload logic ...
        return True
    except FileNotFoundError:
        return False

Example 3: Module-Level Docstring

"""
Utility functions for data validation.

This module provides helpers for common validation patterns:
- `validate_email`: Checks email format.
- `validate_phone`: Checks phone number format.
- `sanitize_input`: Strips dangerous characters.
"""

Example 4: Class Docstring

class BankAccount:
    """
    A simple bank account with deposit and withdraw operations.

    Attributes:
        owner: Name of the account owner.
        balance: Current account balance (maybe negative if overdraft allowed).
    """

    def __init__(self, owner: str, initial_balance: float = 0.0):
        self.owner = owner
        self.balance = initial_balance

    def deposit(self, amount: float) -> float:
        """
        Add funds to the account.

        Args:
            amount: Positive value to deposit.

        Returns:
            The new balance after deposit.

        Raises:
            ValueError: If amount is negative.
        """
        if amount < 0:
            raise ValueError("Deposit amount must be positive")
        self.balance += amount
        return self.balance

Test your understanding: Use print(BankAccount.__doc__) to see the class docstring in action.

Compare Options / When to Choose What

Style Format Best for Example
One-liner """Summary.""" Simple functions (≤3 lines) def add(a, b): """Return a + b."""
Google style Sections: Args, Returns, Raises Medium-to-large internal projects def func(param): """Summary.\n\nArgs: ..."""
NumPy/SciPy style Sections with headings Scientific computing, data science def func(param): """\nSummary.\n\nParameters\n----------\nparam : type\n Description.\n"""
Sphinx/reStructuredText :param: directives Documentation generation (Sphinx) def func(param): """Summary.\n\n:param param: Description"""
  • Use Google style for most daily Python work — it's clean and widely adopted.
  • Use NumPy style if you're in a scientific community (pandas, scikit-learn docs).
  • Use Sphinx style only when you're generating HTML docs with Sphinx.

Comments vs. docstrings: - Docstrings are for every public function, class, and module. - Comments are only for internal non-obvious logic inside a function. - Never use comments to repeat what the code already communicates.

Troubleshooting & Edge Cases

Docstring Over-generality

Problem: You write a docstring that says "processes data" without specifying what data or how. Fix: Be specific: "Validates and transforms user-submitted form data before database insertion."

Stale Docstrings

Problem: You change a function's signature but forget to update its docstring. Fix: Make it a habit to update docstrings during code review. Use linters like pylint (which warns about missing or mismatched docstrings).

Too Many Comments

Problem: Your code is cluttered with comments that explain every line. Fix: Delete comments that state the obvious. Trust clean variable names and function naming.

# Bad:
# x is the radius
x = 5
# multiply by pi and square
area = 3.14 * x ** 2

# Good:
radius = 5
area = 3.14 * radius ** 2

Non-Standard Format Causes Confusion

Problem: Your team mixes Google and NumPy styles, making help() output inconsistent. Fix: Agree on one style for the project. Tools like docformatter can auto-format.

Comments That Lie

Problem: A comment says one thing, but the code does another. Fix: Always treat comments as code — they must be accurate. Outdated comments are worse than no comments.

What You Learned & What's Next

You now understand the critical role of docstrings and comments in making your Python code readable and maintainable. You can: - Write clear one-liner and multi-line docstrings for functions, classes, and modules. - Choose between Google, NumPy, and Sphinx docstring styles. - Use comments to explain why, not what. - Recognize and fix common anti-patterns (stale docstrings, over-commenting, lying comments).

Next step: In the next lesson, "Apply Code Style with PEP 8," you'll learn how to enforce consistent formatting across your entire Python codebase. Combined with readable docstrings, your code will be both understandable and professional. Keep documenting as you go!

Practice recap

Now it's your turn. Open your current Python script or a small project and pick one function that has no docstring. Write a Google-style docstring for it, including a one-line summary, Args, Returns, and Raises if applicable. Then run help(your_function_name) in the Python REPL to see your documentation in action. Repeat this for three different functions to build the habit.

Common mistakes

  • Writing docstrings that only restate the function name, e.g., def add(x, y): """Adds x and y.""" — that's redundant. Instead, describe the purpose: """Return the sum of two numbers."""
  • Failing to update docstrings after changing function parameters or behavior, causing documentation to become misleading over time.
  • Adding comments that explain every line of obvious code, like # Set x to 5 next to x = 5. This adds noise and reduces readability.
  • Using inconsistent docstring styles across a project, which confuses readers and breaks tools like help().
  • Writing overly verbose docstrings that include implementation details already visible in the code, instead of focusing on the interface and usage.

Variations

  1. You can use __doc__ directly in code to access a function's docstring at runtime, which is useful for dynamic help generation or testing.
  2. Some developers prefer the NumPy/SciPy docstring style for scientific projects because it clearly separates sections with headings and types.
  3. Tools like pydocstyle or docformatter can automatically check and format your docstrings to comply with a chosen style, reducing manual effort.

Real-world use cases

  • A web API handler function is documented with Google-style docstrings, so that new developers can understand expected input parameters and return types without reading the entire implementation.
  • A data processing pipeline in a machine learning project uses NumPy-style docstrings to document each transformation step, helping data scientists validate the flow.
  • A shared utility library for a company uses module-level docstrings (with a list of contained functions) so that everyone can discover available helpers via help(module_name).

Key takeaways

  • Docstrings are for users of your code and become part of the official documentation via help(). Comments are for maintainers and explain why, not what.
  • Use the Google docstring style for most projects — it's clean, readable, and widely adopted.
  • Always update docstrings when you change a function's signature or behavior; stale documentation is worse than none.
  • Avoid over-commenting: let clean variable names and function names speak for themselves. Reserve comments for non-obvious logic.
  • Choose one docstring style per project and enforce it with linters or formatters like pydocstyle or docformatter.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.