| name | writing-docstrings |
| description | How to write Python docstrings in TVBO so that quartodoc renders them correctly in the API site. Google style, markdown body, type info on signature (not in the docstring). |
Writing Docstrings for Quarto / quartodoc
The API site at docs/api/** is generated by quartodoc from Python docstrings. Whether your code shows up well in the rendered site depends entirely on how you write them. This skill is the contract.
TL;DR — the canonical form
def render_code(experiment: SimulationExperiment, format: str = "jax") -> str:
"""Render the simulator code for *experiment* without executing it.
Useful for debugging or for running the kernel outside TVBO. Supported
formats are listed in [`tvbo.codegen.registry`](../codegen/registry.qmd).
Args:
experiment: The `SimulationExperiment` to render.
format: Backend identifier (e.g. `"jax"`, `"tvb"`, `"pyrates"`).
Defaults to `"jax"`.
Returns:
The rendered source code as a single string.
Raises:
ValueError: If *format* is not a registered backend.
Examples:
>>> code = render_code(exp, format="jax")
>>> "def integrate" in code
True
"""
That's the shape every docstring should approximate. The rules behind it:
1. Style is Google
The quartodoc config in docs/api/_quartodoc_config.yml sets parser: google. Use Args: / Returns: / Raises: / Yields: / Examples: blocks — not NumPy's Parameters\n----------\n underline form.
Google style is also less line-noise than NumPy and renders predictably as parameter tables in the Quarto site.
2. Type info goes on the signature, not in the docstring
def foo(x):
"""Args:
x (int): The thing.
"""
def foo(x: int) -> str:
"""Args:
x: The thing.
Returns:
Description (no type — quartodoc reads it from `-> str`).
"""
Quartodoc displays the type from the annotation. Repeating it in the docstring desyncs over time and clutters the rendered table.
Exception: if a parameter is Any, object, or accepts heterogenous inputs, document the accepted forms in prose.
3. The body is markdown
Inside the docstring, write CommonMark/Quarto-flavored markdown:
- Backticks for code:
`SimulationExperiment`, `from_db()`
- Links:
[CodeGen](../codegen/index.qmd) for cross-references
- Lists, bold/italic, code blocks — all work
- Math:
$\dot{x} = -\gamma x$ inline, $$ … $$ block
Quarto's renderer processes the body as markdown after quartodoc lifts the parameter blocks into tables.
4. First line is a one-sentence summary
Imperative, period-terminated, fits on one line. This becomes the entry in the module's summary table.
"""Render a SimulationExperiment to YAML."""
"""Renders a SimulationExperiment to YAML"""
"""This function renders a SimulationExperiment."""
After a blank line, free-form prose can elaborate.
5. Args: block — only required when there are parameters
Each parameter gets its own line. Indent continuation lines by 4 spaces.
def foo(x: int, y: str = "z", *args, **kwargs):
"""Summary.
Args:
x: Description.
y: Description that spans
multiple lines — indent the continuation by 4 spaces.
*args: Positional varargs (document if meaningful).
**kwargs: Forwarded to `something.else()`.
"""
Skip self / cls. Don't write Args: at all when the function takes no documented parameters.
Never group multiple parameter names on one line — foo, bar: Both are ... makes quartodoc emit a warning and the entry won't render correctly. Give each parameter its own line, even if the body is duplicated:
Args:
edge_cmap, node_cmap: Matplotlib colormap names.
Args:
edge_cmap: Matplotlib colormap name for edges.
node_cmap: Matplotlib colormap name for nodes.
6. Returns: — describe what comes back
def f() -> dict:
"""Summary.
Returns:
Mapping of region name → connectivity matrix. Empty if the
connectome has no entries.
"""
For multiple return values, document each:
def f() -> tuple[int, str]:
"""Summary.
Returns:
A `(count, label)` tuple where `count` is the number of regions
and `label` is the human-readable atlas name.
"""
Omit Returns: for None-returning functions unless the side effect is the point and worth describing.
7. Raises: — only what callers can sensibly catch
"""Summary.
Raises:
FileNotFoundError: If *path* doesn't exist.
ValueError: If the YAML is malformed.
"""
Don't list RuntimeError, AssertionError, etc. unless they're a deliberate part of the contract. Internal failures are not callers' business.
8. Examples: — choose python vs {python} carefully
Critical for TVBO: the API site executes every ```{python} block on render. A broken example breaks the docs build. There are three legal forms; pick whichever you can guarantee works:
Form A — illustrative code block (default)
Use a plain ```python fence. Quarto renders it as syntax-highlighted code without executing. Choose this when:
- The example references state defined in another method's docstring
- It requires a GUI, network, large data, or interactive input
- You're not 100% sure it runs cleanly in a fresh kernel
"""Summary.
Examples:
```python
exp = SimulationExperiment.from_db("LargeStudy")
exp.report(format="html").open()
```
"""
Form B — executable, self-contained
Use ```{python} (with the curly braces) only if the snippet can run end-to-end with no setup beyond imports on the first line. Each docstring renders to its own page, and Quarto runs each {python} cell in a kernel shared across all cells on that page. Don't rely on state from another method's example.
"""Summary.
Examples:
```{python}
from tvbo import Dynamics
dyn = Dynamics.from_db("Generic2dOscillator")
print(dyn.name)
```
"""
If the example needs a DB-resolved object, the import + construction must both be inside the cell.
Form C — doctest
>>> lines render as illustrative code (not executed by Quarto). Useful if you also run pytest --doctest-modules:
"""Summary.
Examples:
>>> from tvbo import Dynamics
>>> Dynamics.from_db("Generic2dOscillator").name
'Generic2dOscillator'
"""
Audit
To find broken executable examples before they break the doc build:
grep -rn '```{python}' tvbo/
Each match should either (a) be self-contained with imports, or (b) be downgraded to plain ```python. There is no third option — leaving a half-broken {python} block in a docstring will break quarto render.
9. Classes
Docstring on the class describes the object, not the constructor. __init__ is documented either by inheriting the class docstring or with its own docstring.
class Dynamics:
"""Local population/neural-mass model: parameters, state variables, equations.
A `Dynamics` is the smallest building block of a `SimulationExperiment`.
It binds a name to a set of parameters and an ODE system, and is
round-trippable through YAML.
Args:
name: Display name of the model.
parameters: Mapping of parameter name → `{"value": float, ...}`.
state_variables: Mapping of state-variable name → `{"equation": {...}}`.
Examples:
>>> lorenz = Dynamics(name="Lorenz", parameters={"sigma": {"value": 10}})
"""
def __init__(self, name, parameters=None, state_variables=None):
...
Methods on the class follow the same conventions as standalone functions.
10. Don't write a docstring just to satisfy a linter
If a function genuinely has nothing to say beyond its name (_clamp, _normalize_path), it's almost certainly private — prefix it with _ and quartodoc will skip it.
Public functions that lack a meaningful summary are usually signs the API surface is wrong. Promote it to private, or rename it so the name is the documentation.
11. include_empty: true is a workaround, not a license
The quartodoc config sets include_empty: true because many TVBO classes inherit docstrings from the LinkML-generated parent (where griffe's static parser can't see them). This means classes without docstrings will still appear in the rendered docs as empty entries — they show up as a name with no description in the summary table.
Don't take that as permission to skip docstrings. An empty entry is worse than no entry: it tells users "this thing exists, good luck".
12. Cross-references between pages
To link to another generated page, use the qmd extension:
"""See [`SimulationExperiment`](../classes/experiment.qmd) for the host object."""
Paths are relative to the current .qmd file under docs/api/. To link to a member on another page:
"""See [`SimulationExperiment.run`](../classes/experiment.qmd#tvbo.classes.experiment.SimulationExperiment.run)."""
Quartodoc emits anchor IDs as {full.qualified.path} — use that as the URL fragment.
13. What NOT to write
- Don't repeat the function name in the summary:
"""Render the experiment.""" not """render_experiment renders the experiment."""
- Don't describe parameter types in prose when the annotation already does it
- Don't write
Returns: None for procedures that return nothing
- Don't write change-log / version-history notes in docstrings — those live in
CHANGELOG.md
- Don't include planning notes ("TODO: handle the case where…") — those go in commit messages or issue trackers, not user-facing docs
- Don't copy-paste the same paragraph across overloaded methods — write it once on the base method and let inheritance / cross-references do the rest
14. Auditing coverage
To check which public functions/classes still lack docstrings:
python - <<'PY'
import griffe
mod = griffe.load("tvbo")
def walk(m, prefix="tvbo."):
for name, member in m.members.items():
if getattr(member, "is_alias", False) or not member.is_public:
continue
if member.kind.value == "module":
walk(member, prefix + name + ".")
continue
if member.kind.value in ("class", "function") and not member.docstring:
print(f" {prefix}{name}")
walk(mod)
PY
Run before any PR that touches the API.
See also
writing-code — general coding discipline
running-simulations, writing-models — example skills with concise, link-rich content
- The quartodoc rendering tests show what good output looks like: open
docs/api/utils/report.qmd for the rendered form of tvbo/utils/report.py's docstrings.