Python Docstrings: Document Code Clearly

Published on: July 10, 2026
Reading time: 6 minutes
Pessoa programando em um notebook, vista de trás, com código desfocado exibido na tela em um fundo escuro

Python docstrings explain how modules, functions, classes, and methods should be used. Unlike ordinary comments, a docstring becomes part of the object and can be read by help(), development tools, documentation generators, and other Python code.

This guide explains one-line and multi-line docstrings, parameters, return values, exceptions, classes, modules, properties, examples, style conventions, pydoc, and common documentation mistakes.

Docstrings complement readable code. Use them together with PEP 8 style, type hints, and focused Python functions.

What Is a Docstring?

A docstring is a string literal placed as the first statement in a module, function, class, or method.

def greet(name: str) -> str:
    """Return a friendly greeting for a name."""
    return f"Hello, {name}!"

Python stores this text in the object’s __doc__ attribute:

print(greet.__doc__)
help(greet)

The authoritative conventions are in PEP 257. The official pydoc documentation explains built-in documentation generation.

Docstrings vs Comments

Comments explain implementation decisions to someone reading the source code:

# Keep the original order because the API requires stable output.
records = list(dict.fromkeys(records))

Docstrings explain a public interface:

def remove_duplicates(records: list[str]) -> list[str]:
    """Return unique records while preserving their original order."""
    return list(dict.fromkeys(records))

A comment answers “why is this code written this way?” A docstring usually answers “what does this object do, how do I use it, and what can it return or raise?”

One-Line Docstrings

Use a one-line docstring for a simple object with obvious inputs and outputs:

def square(number: float) -> float:
    """Return the square of a number."""
    return number ** 2

Write the summary as a command: “Return,” “Load,” “Calculate,” or “Validate.” Keep the opening and closing triple quotes on the same line.

Multi-Line Docstrings

Use multiple lines when the function needs context, important behavior, or structured details:

def calculate_total(
    prices: list[float],
    tax_rate: float = 0.0,
) -> float:
    """Calculate the total price including tax.

    Negative prices are rejected. The tax rate is expressed as a
    decimal value, so 0.2 represents twenty percent.
    """
    if any(price < 0 for price in prices):
        raise ValueError("prices cannot be negative")

    subtotal = sum(prices)
    return subtotal * (1 + tax_rate)

The first line is a concise summary. Leave a blank line before the longer explanation.

Document Parameters and Returns

Python does not require one specific structured docstring format. Popular conventions include Google style, NumPy style, and reStructuredText. Choose one for the project and use it consistently.

Google-Style Example

def calculate_discount(
    total: float,
    rate: float,
) -> float:
    """Calculate a discount amount.

    Args:
        total: Original purchase total.
        rate: Decimal discount rate between 0 and 1.

    Returns:
        The amount to subtract from the original total.

    Raises:
        ValueError: If total is negative or rate is outside 0 to 1.
    """
    if total < 0:
        raise ValueError("total cannot be negative")

    if not 0 <= rate <= 1:
        raise ValueError("rate must be between 0 and 1")

    return total * rate

Type hints already show the expected types. The docstring should explain meaning, units, valid ranges, and behavior rather than simply repeat the annotation.

Document Exceptions

Document exceptions callers are expected to handle:

from pathlib import Path


def load_config(path: Path) -> str:
    """Load UTF-8 configuration text.

    Args:
        path: Location of the configuration file.

    Returns:
        The complete file contents.

    Raises:
        FileNotFoundError: If the configuration file does not exist.
        PermissionError: If the file cannot be read.
        UnicodeDecodeError: If the content is not valid UTF-8.
    """
    return path.read_text(encoding="utf-8")

Do not list every internal exception that cannot reasonably escape the function. Focus on the public contract.

Document a Class

A class docstring should explain what the object represents and how it is intended to be used:

class BankAccount:
    """Represent a bank account with a non-negative balance.

    Attributes:
        owner: Name of the account owner.
        balance: Current account balance.
    """

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        if balance < 0:
            raise ValueError("balance cannot be negative")

        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float) -> None:
        """Add a positive amount to the account balance."""
        if amount <= 0:
            raise ValueError("amount must be positive")

        self.balance += amount

Avoid repeating the complete class description in every method. Each method should document its own contract.

Document __init__ or the Class?

Many projects place constructor parameter documentation in the class docstring. Some documentation tools and conventions prefer an __init__ docstring. Follow the selected project style and avoid duplicating the same text in both places.

Module Docstrings

A module docstring appears at the top of a Python file, before imports except for a shebang or encoding declaration.

"""Utilities for loading and validating sales reports.

The module provides functions for reading CSV data, validating required
columns, and calculating totals. It does not perform network requests.
"""

from pathlib import Path

import pandas as pd

A useful module docstring describes the module’s responsibility, major public objects, important side effects, and constraints.

Package Docstrings

A package can include a docstring in its __init__.py file:

"""Tools for processing and exporting financial reports."""

For a larger package, mention the primary submodules or the recommended public imports.

Property Docstrings

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @property
    def area(self) -> float:
        """Return the rectangle area."""
        return self.width * self.height

Tools often display the getter’s docstring as the property documentation.

Include Examples When They Add Value

def normalize_email(email: str) -> str:
    """Return a normalized email address.

    The function removes surrounding whitespace and converts the address
    to lowercase.

    Examples:
        >>> normalize_email("  [email protected] ")
        '[email protected]'
    """
    return email.strip().lower()

Examples are helpful when the output format, edge cases, or intended workflow are not obvious.

Test Examples with doctest

Python’s doctest module can run examples written with interactive prompts:

python -m doctest -v email_utils.py

Doctest works well for small deterministic examples. It is not a replacement for a complete test suite. Use Pytest for broader tests and complex setup.

Access Docstrings at Runtime

import inspect

print(inspect.getdoc(normalize_email))

inspect.getdoc() cleans indentation and can inherit documentation in some class scenarios, making it more convenient than reading __doc__ directly.

Generate Documentation with pydoc

View terminal documentation:

python -m pydoc your_module

Generate an HTML file:

python -m pydoc -w your_module

Start a local documentation browser:

python -m pydoc -b

For larger projects, tools such as Sphinx and MkDocs can build searchable websites from docstrings and Markdown or reStructuredText files.

Do Not Repeat Type Hints

This docstring adds little value:

def find_user(user_id: int) -> dict:
    """Find a user.

    Args:
        user_id (int): The user ID integer.

    Returns:
        dict: A dictionary.
    """
    ...

A stronger version explains semantics:

def find_user(user_id: int) -> dict:
    """Return the active user with a database identifier.

    Args:
        user_id: Positive internal user identifier.

    Returns:
        A user record containing name, email, and role.

    Raises:
        LookupError: If no active user has the identifier.
    """
    ...

The type hints guide explains how annotations and documentation work together.

Document Behavior, Not Implementation

A docstring should remain useful even when the internal implementation changes.

# Too implementation-specific
"""Loop over rows and append valid entries to a new list."""

# Better public contract
"""Return records that contain all required fields."""

Implementation details belong in code and selective comments unless callers depend on them.

Document Side Effects

Callers should know when a function writes files, sends requests, changes global state, or mutates an argument.

def save_report(report: str, output_path: Path) -> None:
    """Write a report to a UTF-8 text file.

    Existing content at output_path is replaced.

    Args:
        report: Complete report text.
        output_path: Destination file.
    """
    output_path.write_text(report, encoding="utf-8")

For file operations, see the pathlib guide.

Document Optional and Sentinel Values

def find_product(product_id: int) -> dict | None:
    """Return a product record or None when it does not exist."""
    ...

Explicitly documenting None prevents callers from treating the result as a dictionary unconditionally. The booleans guide explains None and truth-value checks.

Document Async Functions

async def fetch_profile(user_id: int) -> dict:
    """Fetch a user profile from the remote service.

    Args:
        user_id: Positive remote user identifier.

    Returns:
        The decoded profile response.

    Raises:
        TimeoutError: If the service does not respond in time.
        LookupError: If the remote user does not exist.
    """
    ...

Mention cancellation, retries, rate limits, or network effects when they are part of the interface. The asyncio guide covers asynchronous behavior.

Practical Example: A Documented API Client

from typing import Any

import requests


class PostClient:
    """Retrieve posts from a JSON HTTP API.

    Args:
        base_url: API root without a trailing slash.
        timeout: Maximum request duration in seconds.
    """

    def __init__(self, base_url: str, timeout: float = 10.0) -> None:
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    def get_post(self, post_id: int) -> dict[str, Any]:
        """Return one post from the API.

        Args:
            post_id: Positive post identifier.

        Returns:
            The decoded JSON post record.

        Raises:
            ValueError: If post_id is not positive.
            requests.RequestException: If the request fails.
        """
        if post_id < 1:
            raise ValueError("post_id must be positive")

        response = requests.get(
            f"{self.base_url}/posts/{post_id}",
            timeout=self.timeout,
        )
        response.raise_for_status()
        return response.json()

The docstrings define the public behavior while type hints describe expected data shapes. See the Python Requests guide for HTTP details.

Keep Documentation Updated

An outdated docstring is often worse than no docstring because it creates false confidence. Update documentation in the same change that modifies behavior.

Useful review questions include:

  • Does the summary still describe the current behavior?
  • Are parameters missing or renamed?
  • Can the function now return None?
  • Are new exceptions visible to callers?
  • Did a side effect or file format change?
  • Does the example still run?

Lint Docstrings

Tools can check missing or malformed documentation. Common options include pydocstyle, Ruff’s docstring rules, and documentation extensions for Sphinx.

[tool.ruff.lint]
select = ["E", "F", "D"]

Enable rules gradually. Requiring documentation for every private helper may create noise in a small project, while public libraries usually need stronger coverage.

What Should Be Documented?

Prioritize:

  • Public modules, classes, functions, and methods.
  • Non-obvious behavior and constraints.
  • Side effects and destructive operations.
  • Units, ranges, formats, and encoding requirements.
  • Expected exceptions and missing-result behavior.
  • Examples for interfaces that are easy to misuse.

Simple private helpers may need only a clear name and readable implementation.

Common Mistakes

  • Placing the string after executable code instead of first.
  • Writing a comment where a public docstring is needed.
  • Repeating type hints without explaining meaning.
  • Documenting implementation instead of behavior.
  • Listing exceptions that cannot escape the function.
  • Forgetting side effects such as overwriting a file.
  • Using different docstring styles in the same project.
  • Allowing examples and parameter names to become outdated.
  • Writing long documentation for code that should first be simplified.

Frequently Asked Questions

Are docstrings required by Python?

No. The interpreter runs code without them, but public and reusable code benefits greatly from clear documentation.

Should every function have a docstring?

Document public and non-obvious functions. A tiny private helper with a precise name may not need one.

Which docstring style should I use?

Google, NumPy, and reStructuredText styles are all common. Use the style supported by your team and documentation tools.

Can docstrings replace type hints?

No. Type hints support static analysis and editor tooling, while docstrings explain meaning, behavior, constraints, and examples.

Can docstrings be tested?

Interactive examples can be tested with doctest. Broader behavior should be covered by an ordinary test suite.

Conclusion

Python docstrings turn code into a usable interface. Begin with a clear one-line summary, then document parameters, returns, expected exceptions, side effects, constraints, and examples when they help the caller.

Keep documentation close to the code and update both together. The strongest docstrings do not describe every line—they clarify the contract that users and other developers can rely on.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    2 balões de comentários em um fundo laranja
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Comments: Best Practices and Examples

    Learn Python comments with single-line and inline examples, block explanations, TODO notes, docstrings, security and performance context, review standards, and

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Logo do Python com o texto 'PEP 8' sobre fundo azul escuro, representando o guia de estilo da linguagem
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    PEP 8 in Python: Write Cleaner Code

    Learn PEP 8 in Python with naming, indentation, line length, imports, whitespace, comments, functions, classes, tools, exceptions, and practical refactoring.

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Configuração de logs em aplicações Python usando logging
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Logging: A Complete Beginner’s Guide

    Learn Python logging from scratch: levels, files, formatters, exceptions, handlers, rotation, and practical examples for reliable applications.

    Ler mais

    Tempo de leitura: 10 minutos
    10/07/2026
    Dicas para otimizar scripts Python lentos
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Why Is Your Python Script Slow? Fixes

    Learn why Python scripts run slowly and how to fix it: use built-ins, list comprehensions, sets, generators, NumPy, cProfile profiling,

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026
    Problemas de travamento com threading em scripts Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Threading: Stop Your Script From Freezing

    Learn Python threading: why scripts freeze, create and start threads, use join(), avoid race conditions with Lock, and use ThreadPoolExecutor

    Ler mais

    Tempo de leitura: 4 minutos
    03/06/2026
    Criação de cliente TCP simples usando Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Build a Simple Python TCP Client

    Build a simple Python TCP client using the socket module: connect to a server, send and receive bytes, handle errors,

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026