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
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 1next toi += 1adds 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 5next tox = 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
- You can use
__doc__directly in code to access a function's docstring at runtime, which is useful for dynamic help generation or testing. - Some developers prefer the NumPy/SciPy docstring style for scientific projects because it clearly separates sections with headings and types.
- Tools like
pydocstyleordocformattercan 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
pydocstyleordocformatter.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.