Writing Python code is only half the job. The other half is making sure that code can be understood by the next person who opens the file — and that person is often future you, six months from now, wondering what on earth you were thinking. PEP 287 tackled that problem head-on by proposing reStructuredText as the standard markup format for Python docstrings.
PEP 287 was authored by David Goodger and first published in March 2002. It carries the status "Active," which in Python PEP terms means it is an informational standard that remains in use and has not been superseded. Its sole mission was to end the years-long argument in the Python documentation community about which markup format should be used inside docstrings — and to end it by pointing to reStructuredText, or RST for short.
This article covers what RST docstrings look like, how the format works in practice, where it shows up in tools you probably already use, and when you should reach for it over the alternatives.
What Is PEP 287 and Why Does It Exist
A Python docstring is the string literal that appears as the first statement inside a module, function, class, or method. Python's runtime stores it as the __doc__ attribute on that object, which means any tool — from the interactive interpreter to large documentation generators — can read it without parsing the source code separately. The help() function relies on this entirely.
That built-in support is powerful, but it raises an obvious question: if the docstring is just a plain string, how should developers add structure to it? How do you mark up a parameter name, indicate a return type, call out a warning, or embed a code example in a way that both looks good in the terminal and can be converted to formatted HTML?
Before PEP 287, there was no official answer. Developers improvised. Some used ad-hoc indentation and dashes. Others mimicked JavaDoc. A few experimented with lightweight formats like StructuredText from the Zope project. The result was a fragmented ecosystem where documentation tools couldn't agree on what to parse and how.
PEP 287 is an informational PEP, not a language change. Python itself does not enforce any docstring format at the interpreter level. The PEP establishes a community standard that tools can rely on, not a syntax rule that Python will reject.
PEP 287's answer was reStructuredText, a markup language developed as part of the Docutils project. RST was already in use for writing Python Enhancement Proposals themselves, so it had a working parser, an active maintainer, and real-world testing. The case for adopting it for docstrings was straightforward: it was readable as plain text, expressive enough to produce rich HTML, and it came with tooling ready to go.
The Problem It Solved: A History of Docstring Chaos
To understand why PEP 287 was necessary, it helps to look at what came before it. The Python Documentation Special Interest Group, the Doc-SIG, had been wrestling with the docstring markup question since at least 1996. A number of formats were proposed and debated over those years.
XML and HTML were considered and rejected quickly. They are explicit and machine-readable, but they are painful to write by hand and nearly unreadable in raw form. If you open a Python file in an editor and see XML tags in a docstring, you are going to have a bad time understanding the code at a glance. The same objection applies to TeX.
Perl's POD format was noted as a comparison point. POD works well for Perl, but it mirrors Perl's own reputation for being write-only: functional for the author, less pleasant for anyone else reading the raw source.
JavaDoc was another reference point. It ties documentation directly to HTML, which makes the raw docstrings cluttered with tags. Python's philosophy leans the other direction — code should be readable, and inline documentation is part of the code.
The closest competitor was StructuredText (and its variants, collectively called STexts), which originated at Zope. StructuredText had good ideas but suffered from real problems. Its rules were ambiguous, causing text to be interpreted as markup unintentionally. Its implementations had bugs. It lacked a formal specification independent of the buggy implementation, which meant the spec and the bugs were the same thing. And it had no escape mechanism — if you needed to use a markup character for its literal meaning, you were stuck.
reStructuredText was designed explicitly as a response to those StructuredText problems. It fixed every item on that list: it has a formal specification, unambiguous parsing rules, a working escape mechanism using backslashes, and a deliberately minimal syntax that stays out of the way when you are just reading the source.
reStructuredText Basics for Docstrings
RST is not a large language. You can learn the constructs that appear in docstrings in under an hour. Here are the features that PEP 287 specifically calls out as relevant to the docstring context.
Inline literals and literal blocks
When you want to reference a variable name, function, or any code fragment inline, you wrap it in double backticks. This is the RST convention for inline literals, and it is rendered as monospace text in HTML output.
def connect(host, port, timeout=30):
"""
Open a TCP connection to the specified ``host`` and ``port``.
The ``timeout`` parameter controls how many seconds to wait
before raising a ``TimeoutError``.
"""
For multi-line code examples, RST uses a literal block. You end the preceding paragraph with a double colon (::), then indent the code block. The double colon is converted to a single colon in the output, or dropped entirely if it appears on a line by itself.
def parse_config(path):
"""
Read a JSON configuration file and return its contents as a dict.
Example usage::
config = parse_config("/etc/myapp/config.json")
print(config["database"]["host"])
"""
Doctest blocks
RST recognizes Python interactive session syntax directly. Any block that begins with >>> is treated as a doctest block — no special markers required beyond the standard Python REPL prompt.
def add(a, b):
"""
Return the sum of two numbers.
>>> add(2, 3)
5
>>> add(-1, 1)
0
"""
return a + b
This is useful because Python's built-in doctest module can find and execute these blocks automatically, meaning your documentation examples double as runnable tests.
Field lists for parameters and return values
RST uses a field list syntax derived from RFC 2822 headers to document parameters, return values, exceptions, and other structured information. The field name is surrounded by colons on both sides.
def divide(numerator, denominator):
"""
Divide numerator by denominator and return the result.
:param numerator: The number to be divided.
:type numerator: float
:param denominator: The number to divide by. Must not be zero.
:type denominator: float
:returns: The quotient of the two numbers.
:rtype: float
:raises ZeroDivisionError: If denominator is zero.
"""
if denominator == 0:
raise ZeroDivisionError("denominator cannot be zero")
return numerator / denominator
Sphinx, the documentation generator used by the Python standard library and thousands of third-party packages, reads RST field lists natively and renders them as structured parameter tables in HTML. If you write RST docstrings, you get formatted API docs essentially for free.
Interpreted text and roles
Single backticks in RST denote "interpreted text," which is context-dependent. In the docstring context defined by PEP 287, a single-backtick identifier is treated as a Python identifier and can be linked to its documentation automatically by tools like Sphinx.
You can also use explicit roles to classify the type of the reference. A role is written as a prefix or suffix to the backtick-quoted text:
"""
Extend `Storer`.
Use :meth:`storedata` to add items, and access results via
:attr:`data`.
"""
Common Sphinx roles include :func:, :class:, :meth:, :attr:, :mod:, and :exc:. When Sphinx builds your documentation, it replaces these with clickable hyperlinks pointing to the relevant documented object.
Directives for notes, warnings, and images
RST has an extensible directive system. Directives are block-level commands that begin with two dots and a space. In docstrings, the most commonly used directives are admonitions — formatted callout boxes in the rendered output.
def delete_user(user_id):
"""
Permanently delete a user account and all associated data.
.. warning::
This operation is irreversible. The user's data cannot be
recovered after this function returns successfully.
:param user_id: The unique identifier of the user to delete.
:type user_id: int
"""
Other useful directives include .. note::, .. deprecated::, .. versionadded::, and .. versionchanged::. Sphinx recognizes all of these and renders them with appropriate visual treatment in the generated documentation.
Real-World Examples: RST Docstrings in Practice
The best way to understand how RST docstrings work in production is to look at code that actually uses them. Here are several scenarios drawn from real application patterns.
A data processing class
class DataPipeline:
"""
Orchestrate a sequence of data transformation steps.
Each step is a callable that accepts a ``pandas.DataFrame``
and returns a transformed ``pandas.DataFrame``. Steps are
executed in the order they are registered.
Example::
pipeline = DataPipeline()
pipeline.add_step(drop_nulls)
pipeline.add_step(normalize_columns)
result = pipeline.run(raw_df)
.. note::
Steps are applied lazily. No transformation occurs until
:meth:`run` is called.
"""
def add_step(self, func):
"""
Register a transformation step.
:param func: A callable that accepts and returns a DataFrame.
:type func: callable
:raises TypeError: If ``func`` is not callable.
"""
def run(self, dataframe):
"""
Execute all registered steps in order.
:param dataframe: The input data to transform.
:type dataframe: pandas.DataFrame
:returns: The fully transformed DataFrame.
:rtype: pandas.DataFrame
"""
A utility function with doctest examples
def slugify(text, separator="-"):
"""
Convert a string to a URL-safe slug.
Lowercases the text, strips leading and trailing whitespace,
replaces spaces and underscores with ``separator``, and removes
any character that is not alphanumeric or the separator.
:param text: The input string to convert.
:type text: str
:param separator: The character used to replace spaces. Defaults to ``"-"``.
:type separator: str
:returns: A URL-safe slug string.
:rtype: str
>>> slugify("Hello World")
'hello-world'
>>> slugify("Python 3.12 Release Notes", separator="_")
'python_312_release_notes'
>>> slugify(" Leading spaces ")
'leading-spaces'
"""
A module-level docstring
RST docstrings are not just for functions and classes. Module-level docstrings benefit from the same formatting, and they appear prominently in Sphinx-generated documentation.
"""
auth.tokens
===========
Utilities for generating, validating, and revoking authentication tokens.
This module implements HMAC-SHA256 signed tokens with configurable
expiration. Tokens are opaque to the client and validated server-side
on every request.
.. versionadded:: 2.0
Token revocation support via :func:`revoke_token`.
.. deprecated:: 3.1
The ``legacy_validate`` function will be removed in version 4.0.
Use :func:`validate_token` instead.
"""
The Tooling That Makes It Worth It
RST docstrings are not just a style preference. They are the native input format for several tools that are central to the Python ecosystem.
Sphinx is the documentation generator used by the Python standard library, Django, Flask, NumPy, SQLAlchemy, and most major open-source Python projects. When you run Sphinx with the autodoc extension enabled, it imports your modules, reads their docstrings, and generates a full HTML documentation site. RST is the native format — no conversion layer, no plugin required. Field lists like :param: and :rtype: are rendered as proper parameter tables.
Docutils is the underlying library that implements the RST parser. It is what Sphinx uses internally, and it is also available directly if you want to render RST to HTML, LaTeX, or plain text outside of Sphinx.
Read the Docs is the hosting platform that builds and publishes Sphinx documentation automatically on every commit. It is free for open-source projects. If your codebase uses RST docstrings and Sphinx configuration, deploying documentation to Read the Docs is a matter of connecting your repository.
IDEs and language servers also benefit from structured docstrings. PyCharm and VS Code with Pylance both parse RST-style field lists to display parameter information in tooltips when you hover over a function call. The more complete your RST docstrings, the richer that inline help becomes.
pydoc, Python's built-in documentation tool, treats all docstrings as preformatted plain text, so RST markup will appear literally in terminal output. This is by design — RST is readable in raw form, so the help() output is still useful even without rendering.
RST vs. NumPy vs. Google Style
PEP 287 is a standard recommendation, not a hard requirement. Two other docstring styles have become widely used, and it is worth understanding where they came from and when each makes sense.
The NumPy docstring style was developed by the NumPy and SciPy projects to handle the special demands of scientific computing documentation — particularly the need to document large numbers of array parameters with detailed shape and dtype information. NumPy-style docstrings use plain text section headers separated by dashes rather than RST field lists. The Sphinx extension numpydoc parses this format and renders it properly.
def convolve(a, v, mode="full"):
"""
Returns the discrete, linear convolution of two one-dimensional sequences.
Parameters
----------
a : array_like
First one-dimensional input array.
v : array_like
Second one-dimensional input array.
mode : str, optional
One of 'full', 'valid', or 'same'. Default is 'full'.
Returns
-------
out : ndarray
Discrete, linear convolution of a and v.
"""
The Google docstring style uses indented sections with plain text headers. It was popularized by Google's internal Python style guide and is supported in Sphinx via the napoleon extension (which is now bundled with Sphinx itself).
def fetch_data(url, timeout=10):
"""Fetch JSON data from a remote URL.
Args:
url (str): The URL to request.
timeout (int): Request timeout in seconds. Defaults to 10.
Returns:
dict: The parsed JSON response body.
Raises:
requests.HTTPError: If the response status is 4xx or 5xx.
"""
Use RST (PEP 287) if you are already using Sphinx without the napoleon extension, if your project targets the broader Python ecosystem, or if you want zero-configuration compatibility with Sphinx autodoc. Use Google or NumPy style if your team finds them more readable in raw form, and use Sphinx napoleon to render them. The important thing is to pick one and apply it consistently across your codebase.
All three formats share the same underlying goal that PEP 287 articulated: documentation that is readable in plain text, structured enough to be processed into rich output formats, and consistent enough that tools can rely on it. The RST format is the original standard; Google and NumPy styles are variations that trade some RST richness for perceived readability in raw form.
One practical consideration: RST field list syntax is more compact for short parameter lists, while Google and NumPy styles tend to scale better visually when documenting many parameters with complex descriptions. For APIs that expose dozens of parameters — as is common in machine learning libraries — the horizontal space that Google and NumPy styles provide is genuinely useful.
Key Takeaways
- PEP 287 solved a real problem: Before it, the Python community had no agreed-upon format for docstring markup, which fragmented the tooling ecosystem and produced inconsistent documentation across projects.
- RST is human-readable by design: Unlike XML or HTML, reStructuredText reads naturally as plain text. The
help()function output is useful even without any rendering step, which aligns directly with how Python programmers interact with code in the REPL. - Field lists are the core of RST docstrings: The
:param:,:type:,:returns:,:rtype:, and:raises:field list syntax is what Sphinx and IDEs parse to produce structured parameter documentation and rich hover tooltips. - Doctest blocks are a free bonus: RST docstrings with
>>>examples are automatically discovered by Python'sdoctestmodule, giving you executable, testable documentation with no extra setup. - The standard is active, not historical: PEP 287 carries "Active" status. RST docstrings remain the native format for Sphinx, which is used by the Python standard library and a large share of the open-source Python ecosystem. Learning to write them correctly pays off every time you ship a library or publish documentation.
Documentation is the least glamorous part of software development, right up until the moment someone else has to use what you built. PEP 287 drew a clear line in the sand and said: this is the format, this is the tooling, let's stop arguing and start documenting. Two decades later, the ecosystem that grew from that decision — Sphinx, Read the Docs, the napoleon extension, IDE integrations — is extensive and genuinely useful. Writing RST docstrings is not just compliance with a standard. It is buying into a system that generates professional documentation from the comments you were going to write anyway.