Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Write Readable Code with Comments

Learn to write readable Python code with comments and docstrings. This lesson covers best practices for inline comments, block comments, and docstrings to improve code clarity and maintainability.

Focus: write readable code with comments and docstrings

Sponsored

Have you ever revisited code you wrote just a few weeks ago and struggled to understand what it does? Or inherited a script from a colleague that works like magic but leaves you guessing the why behind every line? This is the silent productivity killer in every Python project — code that runs correctly but remains opaque, brittle, and hard to maintain. The fix isn't more clever logic; it's intentional documentation through comments and docstrings, turning your code into a self-explanatory story.

The problem this lesson solves

Uncommented code might work perfectly today, but as projects grow, the cost of deciphering intent explodes. A missing # why this offset? can waste hours of debugging. Relying on memory or external specs is a recipe for tech debt. Comments and docstrings are the why behind the what — they make your code readable, maintainable, and team-friendly without slowing you down.

Core concept / mental model

Think of comments as stick notes for yourself and docstrings as user manuals for your functions, classes, and modules.

  • A comment (#) explains why a tricky line exists. It answers “Why this approach?” not “What does this line do?” — the code already shows what.
  • A docstring (triple-quoted string """...""") documents what a function/module/class does, its parameters, return value, and side effects. It’s meant for other developers (and tools like help()).

Mental image: Comments are like sticky notes on a dashboard; docstrings are the official owner's manual.

How it works step by step

Inline comments: the "why" not the "what"

Bad — comment repeats the code:

# Increment counter by 1
i += 1

Good — comment explains the business rule:

# Skip Monday because the data feed is unreliable that day
if day_of_week == "Monday":
    continue

Block comments: describe a section

Use a block comment before a complex block to set context. Keep it under 5 lines.

# The following section normalizes timestamps from UTC to local time.
# We apply the user's configured timezone offset, handling DST.
# This is batch-run daily, so we batch by date.
for record in daily_records:
    local_ts = apply_tz(record.utc_ts, user_tz)
    ...

Docstrings: the contract

Write docstrings for all public functions, classes, and modules. Use triple double-quotes """...""". Follow PEP 257 and optionally Google/NumPy/Sphinx style.

Basic docstring — one line:

def calculate_discount(price: float, rate: float = 0.1) -> float:
    """Return the discounted price given a percentage rate."""
    return price * (1 - rate)

Multi-line docstring with parameter descriptions:

def connect_to_db(host: str, port: int) -> bool:
    """
    Establish a connection to the database.

    Args:
        host: Server hostname or IP.
        port: TCP port number.

    Returns:
        True if connection succeeded, False otherwise.
    """
    ...

Module-level docstrings

Every .py file starts with a docstring explaining its purpose:

"""Utility functions for data cleaning and validation.

This module provides helpers for stripping whitespace, checking types,
and normalizing string formats across the pipeline.
"""

Hands-on walkthrough

Let's refactor a messy script into readable code using comments and docstrings.

Before — no comments, no docstrings

def f(x):
    return x * x + 2 * x + 1

def process(items):
    r = []
    for i in items:
        if i > 0:
            r.append(f(i))
    return r

After — comments + docstrings

"""Script for processing positive integers with a quadratic transform."""


def quadratic_transform(x: int) -> int:
    """
    Apply f(x) = x² + 2x + 1 to the input.

    This is the standard quadratic kernel used in our feature engineering.

    Args:
        x: Integer value to transform.

    Returns:
        The transformed integer.
    """
    return x * x + 2 * x + 1


def process_positive_values(values: list[int]) -> list[int]:
    """
    Filter positive numbers and apply the quadratic transform.

    Only integers greater than zero are processed. Negative values
    and zero are silently skipped.

    Args:
        values: List of integers (may include non-positive).

    Returns:
        List of transformed positive integers.
    """
    # Early return for empty input to avoid unnecessary iteration
    if not values:
        return []

    result = []
    for val in values:
        if val <= 0:
            # Skip non-positive values per business rule
            continue
        result.append(quadratic_transform(val))
    return result

Expected output:

>>> process_positive_values([-2, 0, 3, 5])
[16, 36]   # because f(3)=9+6+1=16, f(5)=25+10+1=36

Compare options / when to choose what

Feature Comments (#) Docstrings (""")
Audience Developers reading the code Consumers of the API (via help(), docs)
Scope Single line or block within code Function, class, module level
Content “Why this line?” “What it does, parameters, returns, examples”
Tooling Static analysis ignores them pydoc, Sphinx, IDEs auto-complete from them
Maintenance burden Medium (can drift from code) High (must stay in sync)
When to use Complex logic, temporary workaround, business rule Public API, reusable functions, modules

Rule of thumb: If you can’t infer the purpose from the function signature, write a docstring. Use comments only for non-obvious decisions inside the function.

Troubleshooting & edge cases

🔴 Comment is outdated

Symptom: The comment says # Multiply by 2 but the code divides. Fix: Review comments during code review; treat them as code, not decoration.

🔴 Docstring is too verbose

Symptom: Five paragraphs explaining trivial logic. Fix: One-line docstrings are fine for simple functions ("""Return the timestamp in ISO format."""). Save detail for complex logic.

🔴 No docstring on a public function

Symptom: help(my_func) returns nothing. Fix: Add at least a one-line docstring. Many linters (e.g., pylint, flake8-docstrings) warn about missing docstrings.

🔴 Using # inside a docstring

Problem: Inside a docstring, # is just a character — tools won’t treat it as a comment. Fix: Use narrative text inside docstrings, not # markers.

🔴 Docstring includes implementation details

Bad: """Loops over list and calls external API.""" Reason: Docstrings describe interface, not internal mechanics. Fix: State the effect: """Fetches user data from the API for each ID."""

What you learned & what's next

You now understand how to write readable code with comments and docstrings. You learned: - The difference between inline comments (explain why) and docstrings (document what and how to use). - How to write clean docstrings following PEP 257 conventions. - When to use each tool and how to avoid common pitfalls like outdated comments or missing docstrings.

Next, you’ll apply these skills by documenting your own modules and functions, then move on to writing reusable functions where well-documented code becomes the foundation of larger projects.

Practice recap

Practice exercise: Open one of your existing Python scripts and add module-level docstrings, function docstrings to all public functions, and at least three inline comments that explain why (not what). Then run import your_module; help(your_module.your_function) to verify your docstring appears. Check with a linter like pylint --disable=all --enable=missing-docstring your_script.py.

Common mistakes

  • Writing comments that repeat the code e.g., # add 1 to i before i += 1 — same information, zero value.
  • Forgetting to update comments when code changes — a comment that says # double but code now divides is worse than no comment.
  • Using # characters inside docstrings — they are not parsed as comments; use narrative language.
  • Writing docstrings that detail internal implementation instead of describing the public interface.

Variations

  1. Google-style docstrings — widely adopted, supports parameters and returns in a clear structured format.
  2. NumPy-style docstrings — verbose but very readable, common in scientific Python.
  3. Sphinx-style reStructuredText docstrings — enables auto-generated documentation websites.

Real-world use cases

  • Documenting a REST API client's get_user() method so teammates immediately know parameters, return type, and expected status codes.
  • Adding a module-level docstring to a data cleaning script so new hires understand its purpose and pipeline order.
  • Using inline comments to explain why a specific magic number (e.g., offset=3) exists in a legacy accounting calculation.

Key takeaways

  • Use comments (#) for the why — business rules, workarounds, tricky interactions — not for the what that code already shows.
  • Docstrings ("""...""") are the public contract of functions, classes, and modules — always document public API.
  • Follow PEP 257 for docstring conventions; choose a style (Google, NumPy, Sphinx) and stay consistent across your project.
  • Outdated or misleading comments are worse than no comments — treat them as live documentation that must be reviewed.
  • One-line docstrings are sufficient for trivial functions; multi-line docstrings are required for complex logic or public interfaces.
  • Linters (pylint, flake8-docstrings) can enforce docstring presence and style — add them to your CI pipeline.

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.