Writing Docstrings
How to write docstrings that produce clear, complete API documentation.
folio supports Google-style and NumPy-style docstrings. By default, Folio
auto-detects the style for each docstring. You can force a parser with
source.python.docstring_style in docs.yaml (see
Configuration).
Google-style format
Google-style parsing is powered by the docstring_parser library with
Style.GOOGLE. 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. This is useful for projects that mix both
conventions or are migrating from one style to the other.
Docstring sections
folio recognizes the following sections in a docstring. Each section must use the exact heading shown below, followed by a colon (for Google-style) or an underline (for NumPy-style).
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.
"""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
Properties are rendered with a “property” badge. The getter docstring is used as the
description. Parameters (like self) are automatically excluded:
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 with a “staticmethod” badge. 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 with a “classmethod” badge. The cls parameter is
automatically excluded from the documentation, 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
All decorators are recorded and displayed in the generated documentation. Custom decorators appear as-is:
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__
By default, folio documents every public class, function, and constant in a
module. You can restrict what appears in the documentation by defining __all__:
__all__ = ["PublicClass", "public_function"]
class PublicClass:
"""This will be documented."""
class _InternalHelper:
"""This is private by convention but would be documented without __all__."""
def public_function():
"""This will be documented."""
def helper():
"""This will NOT be documented because it's not in __all__."""When __all__ is defined, only the names listed in it are included in the generated
documentation. When __all__ is not defined, all top-level definitions are documented
(including those without a leading underscore).
__all__ must be a simple list or tuple of string literals. folio parses it
statically from the AST, so dynamic constructions like __all__ = get_exports() are not
supported.
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.
Class variables
Annotated assignments inside a class body are documented as class variables:
class Server:
"""An HTTP server.
Attributes are documented from annotated class-level assignments.
"""
host: str = "localhost"
port: int = 8080
max_connections: int = 100These appear in a dedicated “Attributes” section in the class documentation, showing the variable name, type, and default value.
Async functions
Async functions and methods are fully supported. They are rendered with an async
badge to distinguish them from synchronous functions:
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 shown in module overviews and search results. 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 class that demonstrates all the features described in this guide:
"""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.
"""