Examples throughout are drawn from the real Cantera codebase — an open-source toolkit for chemical kinetics, thermodynamics, and transport.
Professor Carole Goble in “Better Software, Better Research”:
One of my favorite #overlyhonestmethods tweets (a hashtag for lab scientists) is Ian Holmes’s “You can download our code from the URL supplied. Good luck downloading the only postdoc who can get it to run, though.”
“It works on my machine” is not reproducible science.
Documentation is what turns code you can run into code others (and future you) can run. It costs little on top of the coding you already do, and pays back many times over:
Most documentation is a small, steady habit — not a separate project.
Documentation lives at several levels — from the whole project down to a single line:
| Level | Audience | Example |
|---|---|---|
| README / guides | Anyone arriving at the repo | README.rst, INSTALL.md |
| API documentation | People calling your code | docstrings → Sphinx site |
| Self-documenting code | People reading your code | clear names, structure |
| Comments | People editing a tricky section | # why, not # what |
| Theory / user guides | Domain users | tutorials, math derivations |
We’ll walk up this ladder, then finish with versioning — how you label releases of all of it.
The README sits at the top of the repository. It is the first — and often only — thing a newcomer reads. A good README answers, quickly:
It is usually surrounded by companion files: LICENSE, INSTALL, CITATION.cff, CODE_OF_CONDUCT, CONTRIBUTING, CHANGELOG.
README.rstNotice the structure — what it is, what it can do, then install and where next. The badges at the top show live status (CI, coverage, DOI, release) at a glance.
|doi| |codecov| |ci| |release|
What is Cantera?
================
Cantera is an open-source collection of object-oriented software tools for
problems involving chemical kinetics, thermodynamics, and transport processes.
Among other things, it can be used to:
* Evaluate thermodynamic and transport properties of mixtures
* Compute chemical equilibrium
* Conduct kinetics simulations with large reaction mechanisms
* Simulate one-dimensional flames
Installation
============
`Installation instructions for the current release of Cantera
<https://cantera.org/stable/install/index.html>`_ are available from the main site.# Package
`package` is a simple Python library that contains a single function
for rescaling arrays.
## Installation
Download the source code and use the package manager
[pip](https://pip.pypa.io/en/stable/) to install `package`:
`pip install .`
## Contributing
Pull requests are welcome. For major changes, please open an issue
first to discuss what you would like to change.
Please make sure to update tests as appropriate.
## License
TBD
...It’s entirely possible to pollute code with unnecessary cruft:
def decay(index, database):
# first, retrieve the decay constants from the database
mylist = database.decay_constants()
# next, try to access an element of the list
try:
d = mylist[index] # gets decay constant at index in the list
# if the index doesn't exist
except IndexError:
# throw an informative error message
raise Exception("value not found in the list")
return dCode written cleanly will have its own voice. Use intelligent naming to make most lines clear without comments, then use comments sparingly to explain reasons or complicated sections:
Every line is narrated. The comments add nothing the code doesn’t already say — and now there are two things to keep in sync.
The names carry the meaning; the single comment records something the reader genuinely needs (units, and the source of the formula).
Naming: a class, variable, or function name should tell you why it exists, what it does, and how it is used.
Simple functions: functions should be small to be understandable and testable; they should only do one thing.
Consistent style: use a consistent, standardized style; e.g., select variable and function names according to the PEP 8 style guide.
| Kind | Convention | Example |
|---|---|---|
| Packages / modules | lowercase |
cantera, composite |
| Classes / Exceptions | CamelCase |
Solution, SolutionArray |
| Functions / methods | snake_case |
restore_data, arrhenius_rate |
| Constants | ALL_CAPS |
R_GAS |
| Internal / private | _leading_underscore |
_import_pandas, _phase |
| Magic methods | __dunder__ |
__init__, __repr__ |
Guidelines: functions are verbs (compute_equilibrium); booleans read as questions (is_ideal, has_transport). Above all — be consistent.
# packages and modules are short and lowercase
packages
modules
# other objects can be long
ClassesUseCamelCase
ExceptionsAreClassesToo
functions_use_snake_case
CONSTANTS_USE_ALL_CAPS
# variable scope is *suggested* by style convention
_single_leading_underscore # internal to module
single_trailing_underscore_ # avoids conflicts with Python keywords
__double_leading_trailing__ # these are magic, like __init__This snippet from cantera/interfaces/cython/cantera/composite.py follows the conventions without a single comment explaining what — CamelCase class, _leading_underscore for the deferred-import helper, snake_case function:
A docstring is a string literal placed immediately after a def, class, or module statement, typically enclosed by three pairs of double quotes:
Unlike a comment, it’s part of the object at runtime — reachable via help(obj), obj? in IPython, and obj.__doc__. Sphinx picks them up too.
Try it on any standard library function: help(print)
A docstring should say what the function does, its parameters, and what it returns. Provide an example of how to use it if it takes many parameters.
def rescale(input_array):
"""Linearly rescale an array to the range [0, 1].
Maps the minimum value of the input to 0 and the maximum to 1,
scaling all other values proportionally in between.
Parameters
----------
input_array : numpy.ndarray
The array of values to rescale.
Returns
-------
numpy.ndarray
An array the same shape as ``input_array``, with values
linearly mapped onto the interval [0, 1].
Examples
--------
>>> import numpy as np
>>> rescale(np.array([1, 2, 3, 4, 5]))
array([0. , 0.25, 0.5 , 0.75, 1. ])
"""Used in Cantera’s doc/sphinx/conf.py:
def executable_script(src_file, gallery_conf):
"""Validate if script has to be run according to gallery configuration.
Parameters
----------
src_file : str
path to python script
gallery_conf : dict
Contains the configuration of Sphinx-Gallery
Returns
-------
bool
True if script has to be executed
"""def function_with_types_in_docstring(param1, param2):
"""Example function with types documented in the docstring.
`PEP 484`_ type annotations are supported. If attribute, parameter, and
return types are annotated according to `PEP 484`_, they do not need to be
included in the docstring:
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
Returns:
bool: The return value. True for success, False otherwise.
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484/
""":param:) styleUsed in Cantera’s composite.py:
Cantera uses both — the napoleon Sphinx extension understands NumPy/Google style, and Sphinx natively understands :param:. Pick one style per project and stick to it.
Sphinx reads your docstrings (via autodoc) plus hand-written pages, and builds a browsable HTML site — this is how https://cantera.org is produced.
This creates docs/ with a conf.py (configuration) and index.rst (the home page). The source directory holds the .rst and .md files you write by hand — user guides, theory manuals — separate from the autogenerated API documentation.
conf.pyextensions = [
"sphinx.ext.autodoc", # pull in docstrings
"sphinx.ext.autosummary", # summary tables
"sphinx.ext.napoleon", # understand NumPy / Google style docstrings
"sphinx.ext.intersphinx", # link to other projects' docs
"sphinx.ext.mathjax", # render LaTeX math
"myst_nb", # Markdown + Jupyter notebooks as pages
]
# Generate stub pages for entries listed in autosummary directives
autosummary_generate = True
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"pandas": ("https://pandas.pydata.org/pandas-docs/version/2.3", None),
}intersphinx and MySTintersphinx is the magic that turns a numpy.ndarray in your docstring into a live link to NumPy’s own documentation.
Installing myst_parser / myst_nb lets you author pages in Markdown — or even render live Jupyter notebooks — instead of reStructuredText.
napoleon parses both NumPy and Google style out of the box— napoleon_numpy_docstring and napoleon_google_docstring both default to True. Set one to False if you want to enforce a single style across the project.
index.rst (the home page)conf.py only configures Sphinx — it doesn’t say what to document. That’s the job of index.rst, the landing page. Out of the box, sphinx-quickstart leaves it nearly empty, with just a toctree. Nothing references your code yet, so no docstrings appear.
api.rst (the generated reference)automodule is what actually imports rescale.rescale and renders every docstring in it. The toctree in index.rst then stitches index → api → rescale.rescale into the site’s navigation.
api.rst?Separation of concerns. index.rst is narrative plus a map; api.rst is generated reference.
With one module you could inline automodule into index.rst — but the moment you add a second module, a tutorial, or examples, splitting them keeps the home page readable and lets each section grow independently.
Note
For autodoc to find your package, it must be importable — e.g. pip install -e . — or you must add its path via sys.path in conf.py.
Cantera wraps this in its build system: doc/SConscript calls sphinx-build -b html ... to produce build/doc/html/index.html.
A documentation site is most useful when it’s online and always current. A GitHub Actions workflow can rebuild and redeploy it on every push:
# .github/workflows/docs.yml
on:
push:
branches: [main]
paths: ["docs/**", "src/**"]
jobs:
docs:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install . sphinx myst-parser
- run: sphinx-build -b html docs/source docs/build
- uses: actions/upload-pages-artifact@v3
with: { path: docs/build }
- uses: actions/deploy-pages@v4sphinx.ext.githubpages to extensions in conf.pydocs/requirements.txt (or a docs extra in pyproject.toml) for documentation dependenciesThe older pattern — pushing built HTML to a gh-pages branch with an action such as peaceiris/actions-gh-pages — still works, and you’ll see it in many existing projects.
More on Sphinx with GitHub Actions: https://www.sphinx-doc.org/en/master/tutorial/deploying.html
(A deeper example — skip if you’re short on time.)
Docstrings aren’t only for humans reading help(). When they’re structured and machine-readable, a tool can parse them and generate code from them — deterministically (same input → same output, every time).
The payoff is the DRY principle applied to interfaces: document something once, in one place, then have a tool derive a downstream artifact from it automatically — no hand-copying, and no drift between the code and the wrapper around it.
sourcegenCantera’s core is C++. Other languages reach it through a thin C interface, “CLib”. Writing that layer by hand is repetitive boilerplate: for every C++ method you need a C function that looks up the object, converts types, forwards the call, and catches exceptions.
sourcegen generates that CLib layer automatically, by reading the Doxygen docstrings in the C++ headers plus a small YAML recipe listing which methods to wrap:
The C++ method, documented once with Doxygen:
A one-line YAML recipe says what to wrap:
sourcegen emits the C function@param k text carried straight through from the C++ Doxygen comment — docs never drift from the sourcesize_t was crosswalked to C’s int32_t, and a handle argument added for object lookupsourcegen after any C++ change regenerates the whole C APIOnce people depend on your software, they need to know what changed between releases — and whether an update will break them. A version number communicates that.
Common schemes:
MAJOR.MINOR.PATCH2026.060.x 😉MAJOR.MINOR.PATCHGiven a version number, you increment the:
Pre-1.0.0 (0.x.y) means anything may change — the public API is not yet stable. Pre-releases get a suffix like a1 (alpha), b2 (beta), rc1 (release candidate).
Cantera demonstrates this exactly. From SConstruct:
and the current stable release recorded in CITATION.cff:
So 3.2.0 is in the wild, while 4.0.0a1 is an alpha of a major release — the bump from 3.x to 4.0 signals intentional, possibly breaking, API changes.
Define the version once, and derive everything else from it. Three modern approaches:
pyproject.toml:Read it back at runtime (no second copy in source):
setuptools-scm / hatch-vcs generate a _version.py for you (never hand-edited):pyproject.toml
Cantera instead sets it in SConstruct and threads it into the C++ headers, the Python module (cantera.__version__), and the docs (conf.py reads CANTERA_VERSION, so the rendered site is always labelled with the right release). One source of truth, many consumers.
The tag is what triggers the release job in CI, what Zenodo archives to mint a DOI, and what CITATION.cff should agree with.
Documentation is a ladder, and versioning labels each rung’s releases:
MAJOR.MINOR.PATCH tells users what an update meansNone of it is a separate project — it’s a habit layered onto the code you already write.
Comment the why, not the what
Comments provide a way to insert metainformation about code intended for people, right next to the code.
A comment that restates the code is noise; it just goes stale. Comment the things the code can’t say: intent, units, the reason for a non-obvious choice.