Writing Doc Comments
How to write doc comments that produce clear, complete API documentation.
Most of this page is about Python docstrings; JSDoc and Rust doc comments close it.
For Python, Folio supports Google-style and NumPy-style docstrings. By
default, Folio auto-detects the style for each docstring, and the detection
also reads reStructuredText and epydoc docstrings. You can force a parser with
source.python.docstring_style in docs.yaml, which takes "auto",
"google" or "numpy" (see
Configuration).
Google-style format
Google-style parsing is built in. Set source.python.docstring_style to
"google" when you want to force this parser for every docstring:
source:
python:
docstring_style: "google"A basic Google-style docstring looks like this:
def connect(host: str, port: int = 8080, timeout: float = 30.0) -> Connection:
"""Establish a connection to the remote server.
Opens a TCP connection using the specified host and port. The connection
will be kept alive until explicitly closed or the timeout is reached.
Args:
host: The hostname or IP address to connect to.
port: The port number. Defaults to 8080.
timeout: Maximum time in seconds to wait for the connection.
Returns:
A Connection object representing the active connection.
Raises:
ConnectionError: If the server is unreachable.
ValueError: If the host string is empty.
Examples:
>>> conn = connect("localhost")
>>> conn.is_alive()
True
Notes:
This function does not support Unix domain sockets.
"""NumPy-style format
To use NumPy-style docstrings, set source.python.docstring_style to "numpy" in your docs.yaml:
source:
python:
docstring_style: "numpy"A NumPy-style docstring uses section headers with underlines instead of trailing colons:
def connect(host: str, port: int = 8080) -> Connection:
"""Establish a connection to the remote server.
Parameters
----------
host : str
The hostname or IP address.
port : int, optional
The port number, by default 8080.
Returns
-------
Connection
The active connection object.
Raises
------
ConnectionError
If the server is unreachable.
"""The default source.python.docstring_style value is "auto", which detects the
style on a per-docstring basis: it parses each docstring as reStructuredText,
Google, NumPy and epydoc, and keeps whichever reading finds the most
parameters, returns, raises and other fields.
This is useful for projects that mix conventions or are migrating from one
style to the other.
Docstring sections
Folio recognizes the following sections in a docstring. A Google-style heading
is written as shown, capitalized and followed by a colon; a NumPy-style heading
is underlined. Google-style also accepts Arguments:, Parameters: and
Params: for Args:, Exceptions: and Except: for Raises:, Example:
for Examples:, and Note: for Notes:.
Args
Document function parameters under the Args: section. Each parameter is listed on its
own line with an indented description:
def train(data: Dataset, epochs: int = 10, lr: float = 0.001) -> Model:
"""Train a model on the given dataset.
Args:
data: The training dataset.
epochs: Number of training epochs.
lr: Learning rate for the optimizer.
"""Returns
Describe the return value under Returns::
def load(path: str) -> dict:
"""Load configuration from a YAML file.
Returns:
A dictionary containing the parsed configuration.
"""Raises
List exceptions the function may raise under Raises::
def parse(text: str) -> AST:
"""Parse source code into an AST.
Raises:
SyntaxError: If the source code is invalid.
FileNotFoundError: If the source file does not exist.
"""Examples
Provide usage examples under Examples:. These are rendered as code blocks in the
generated docs:
def add(a: int, b: int) -> int:
"""Add two numbers.
Examples:
>>> add(2, 3)
5
>>> add(-1, 1)
0
"""Notes
Add extra context under Notes:. This section is useful for implementation details,
caveats, or related information:
def encrypt(data: bytes, key: bytes) -> bytes:
"""Encrypt data using AES-256-GCM.
Notes:
The nonce is generated randomly and prepended to the ciphertext.
This function is not suitable for encrypting data larger than 2 GiB.
"""Other sections
Attributes: describes a class’s attributes or a module’s constants (see
Class variables). Deprecated:, Warning:, See Also:,
References: and Todo: each render as a labelled paragraph after the
description, such as Deprecated: followed by the section’s text. Sphinx
roles such as :class:`Config` and :meth:`load` read as code.
Type annotations
folio extracts type information from two places, in order of priority:
- Function signatures (preferred) — type annotations on parameters and return types
- Docstring type fields — the
(type)syntax in Google-style docstrings
If a parameter has a type annotation in the function signature, that takes precedence. If there is no signature annotation, folio falls back to the type specified in the docstring.
# Type from signature (preferred)
def greet(name: str) -> str:
"""Greet a user.
Args:
name: The user's name.
"""
# Type from docstring (fallback)
def greet(name):
"""Greet a user.
Args:
name (str): The user's name.
Returns:
str: A greeting message.
"""Both styles produce the same output. When both are present, the signature annotation wins.
Decorators and method kinds
folio detects decorators on functions and methods and renders them differently based on their kind.
@property
A property is marked @property above its heading, its signature is its name,
and its return annotation is shown as its Type. The getter docstring is the
description, and no parameters are listed. A setter or deleter written as
@full_name.setter or @full_name.deleter folds into the property instead of
getting an entry of its own:
class User:
@property
def full_name(self) -> str:
"""The user's full name, computed from first and last name."""
return f"{self.first} {self.last}"@staticmethod
Static methods are marked @staticmethod above their heading. Since they don’t
receive self or cls, all parameters are documented:
class MathUtils:
@staticmethod
def clamp(value: float, low: float, high: float) -> float:
"""Clamp a value to a range.
Args:
value: The value to clamp.
low: Minimum bound.
high: Maximum bound.
"""@classmethod
Class methods are marked @classmethod above their heading. The cls
parameter is left out of the signature and the parameter table, just like
self:
class Config:
@classmethod
def from_file(cls, path: str) -> "Config":
"""Load configuration from a file.
Args:
path: Path to the configuration file.
"""Custom decorators
Every other decorator is shown as written, above the signature:
class EventHandler:
@retry(max_attempts=3)
def handle(self, event: Event) -> None:
"""Handle an incoming event with automatic retry."""*args and **kwargs
Variable positional and keyword arguments are fully supported. They appear in the
documentation with their * or ** prefix implied by their argument kind:
def log(message: str, *args: Any, **kwargs: Any) -> None:
"""Log a message with optional formatting arguments.
Args:
message: The log message template.
*args: Positional arguments for string formatting.
**kwargs: Additional keyword arguments passed to the logger.
"""The parser tracks argument kinds internally: var_positional for *args and
var_keyword for **kwargs. Keyword-only arguments (those after * in the
signature) are tracked as keyword_only.
Controlling what gets documented with __all__
Without __all__, Folio documents every top-level class, function and constant
whose name does not start with an underscore. Defining __all__ restricts the
page to the names it lists, and a listed name is documented even with a
leading underscore:
__all__ = ["PublicClass", "public_function", "_listed_helper"]
class PublicClass:
"""This will be documented."""
class _InternalHelper:
"""Not listed, so not documented; without __all__ its underscore keeps it out."""
def public_function():
"""This will be documented."""
def helper():
"""This will NOT be documented because it's not in __all__."""
def _listed_helper():
"""Documented: __all__ lists it."""Inside a class, a member whose name starts with an underscore is skipped, with
two exceptions: __init__ is always documented, and any other dunder method,
such as __call__ or __eq__, is documented when it has a docstring. When a
function has @overload stubs, only its implementation is shown.
__all__ must be a simple list or tuple of string literals, annotated or
not. Folio parses it statically from the AST, so a dynamic construction like
__all__ = get_exports() is not read, and the module falls back to the
underscore rule.
Nested classes
folio fully supports nested (inner) classes. They are parsed recursively and appear as sub-sections within their parent class documentation:
class Model:
"""A machine learning model.
The Model class supports nested configuration via inner classes.
"""
class Config:
"""Model configuration.
Args are defined as class variables with type annotations.
"""
learning_rate: float = 0.001
batch_size: int = 32
class Callbacks:
"""Lifecycle callbacks for training."""
def on_epoch_end(self, epoch: int, metrics: dict) -> None:
"""Called at the end of each training epoch.
Args:
epoch: The epoch number that just completed.
metrics: Dictionary of metric names to values.
"""Inner classes have their own methods, class variables, and can even contain further nested classes.
Module-level constants
folio documents module-level constants in two ways:
With type annotations — any top-level annotated assignment is captured:
DEFAULT_TIMEOUT: float = 30.0
MAX_RETRIES: int = 3
API_BASE_URL: str = "https://api.example.com"Without type annotations — only UPPER_CASE names (without a leading underscore) are captured:
VERSION = "1.0.0" # documented (UPPER_CASE)
DEFAULT_PORT = 8080 # documented (UPPER_CASE)
_internal_cache = {} # NOT documented (leading underscore)
some_config = "value" # NOT documented (not UPPER_CASE)The constant’s name, type (if annotated), and assigned value are all shown in the
generated documentation. An Attributes: section in the module docstring gives
a constant its description.
Class variables
Assignments inside a class body, annotated or not, are documented as class attributes, and so are an enum’s members:
class Server:
"""An HTTP server.
Attributes:
host: The interface to bind.
port: The TCP port.
workers (int): Worker processes, set in __init__.
"""
host: str = "localhost"
port = 8080They appear in a table in the class section with each attribute’s name, type,
value and description. The description comes from the class docstring’s
Attributes: section, whose type fills in when the assignment has no
annotation; an attribute the section names but the class body does not
assign, such as one set in __init__, gets a row without a value.
Async functions
Async functions and methods are fully supported. Their signature keeps the
async keyword, as in async def fetch(url: str, timeout: float = 10.0):
async def fetch(url: str, timeout: float = 10.0) -> Response:
"""Fetch a resource from the given URL.
Args:
url: The URL to fetch.
timeout: Maximum time to wait in seconds.
Returns:
The HTTP response.
Raises:
TimeoutError: If the request exceeds the timeout.
"""Tips for writing good docstrings
Start with a one-line summary. The first line of your docstring becomes the short
description: a module’s is its description in the API index, the page metadata
and llms.txt. Keep it concise and action-oriented (e.g., “Connect to the
database” not “This function connects to the database”).
Separate the summary from the body. Leave a blank line between the summary and the
longer description. The parser treats the first line as short_description and
everything after the blank line as long_description.
Document all public parameters. Every parameter that appears in the function
signature should have a corresponding entry in the Args: (Google) or Parameters
(NumPy) section. Missing descriptions will render as empty in the docs.
Use type annotations in signatures. Prefer def foo(x: int) over documenting types
in the docstring. Signature annotations are more reliable and are checked by type
checkers like mypy.
Keep examples runnable. Examples in the Examples: section should be valid Python
that a reader could copy-paste. Use >>> prefix for interactive-style examples.
Document exceptions explicitly. If your function raises exceptions, list them in the
Raises: section. This helps users write proper error handling code.
Complete example
Here is a well-documented module that brings the Google-style features of this guide together:
"""Connection management for the RPC framework.
This module provides connection pooling and lifecycle management
for RPC clients.
"""
__all__ = ["ConnectionPool", "DEFAULT_POOL_SIZE"]
DEFAULT_POOL_SIZE: int = 10
class ConnectionPool:
"""A thread-safe pool of reusable connections.
The pool maintains a set of open connections and distributes them
to callers on demand. Connections are returned to the pool when
released.
Examples:
>>> pool = ConnectionPool("localhost", max_size=5)
>>> async with pool.acquire() as conn:
... await conn.execute("SELECT 1")
Notes:
The pool does not currently support connection health checks.
"""
max_size: int = DEFAULT_POOL_SIZE
timeout: float = 30.0
class Stats:
"""Pool usage statistics."""
active: int = 0
idle: int = 0
def __init__(self, host: str, port: int = 5432, max_size: int = 10) -> None:
"""Initialize the connection pool.
Args:
host: Database hostname.
port: Database port.
max_size: Maximum number of connections in the pool.
"""
async def acquire(self, timeout: float | None = None) -> Connection:
"""Acquire a connection from the pool.
Args:
timeout: Maximum time to wait for a connection.
If None, uses the pool's default timeout.
Returns:
An open database connection.
Raises:
PoolExhaustedError: If no connection is available within the timeout.
"""
@property
def size(self) -> int:
"""The current number of connections in the pool."""
@classmethod
def from_url(cls, url: str) -> "ConnectionPool":
"""Create a pool from a database URL.
Args:
url: A connection string like ``postgresql://host:port/db``.
"""
@staticmethod
def is_valid_url(url: str) -> bool:
"""Check whether a connection string is valid.
Args:
url: The connection string to validate.
"""JSDoc
The JavaScript reader takes the /** ... */ comment written directly above an
exported declaration, with no blank line between them. A file’s first comment
documents the module when a blank line separates it from the code below.
/**
* Connect to a server.
*
* Opens a connection; see {@link Connection} for what it returns.
* @param {string} host - The hostname.
* @param {Object} [options] - Connection options.
* @param {number} [options.port=8080] - The port.
* @returns {Connection} The open connection.
* @throws {TypeError} If the host is empty.
* @deprecated Use open instead.
* @example
* connect("localhost", { port: 9000 })
*/
export function connect(host, options = {}) {}The description comes first, then one parameter row per @param: [name]
is read as the parameter’s name (the row does not mark it optional),
[name=default] gives its default, and options.port becomes a row of its own
after options. @returns (or @return) and @throws fill the return and
raise lines, @deprecated renders as a Deprecated: paragraph, and each
@example becomes a code block.
{@link Target} reads as Target in code, {@link Target text} or
{@link Target|text} as its text, and a URL target as a link. A parameter
without a JSDoc type shows Any. Other tags are not read.
Rust doc comments
The Rust reader takes /// and #[doc = "..."] comments on an item and //!
comments for the module, and reads their Markdown as rustdoc does:
- a code block without a language, or tagged only with rustdoc attributes
such as
no_runorshould_panic, is Rust; - lines starting with
#inside a Rust block are hidden, as in rustdoc output, and##at the start of a line stands for a literal#; - intra-doc links such as
[`Config`],[the config][Config]and[text](crate::Config)keep their text, and URL links stay links; - headings such as
# Examplesand# Panicssit below the item’s own heading.
A Rust function shows its signature as written, with no linked types, and its
doc comment; there is no parameter table. #[doc = include_str!("...")] is not
read in this release.