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 ** 2Write 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 * rateType 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 += amountAvoid 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 pdA 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.heightTools 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.pyDoctest 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_moduleGenerate an HTML file:
python -m pydoc -w your_moduleStart a local documentation browser:
python -m pydoc -bFor 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.






