Skip to content

Technical style guide

This guide is for the maintainers of Future Ancestors projects.

It describes how we write commentary in our source code and technical content: Python docstrings and JavaScript JSDoc blocks. It complements the prose style guide, which covers other content: the editorial and narrative writing intended for people to read.

We have deliberately written this guide to work well with AI assistance. Feel free to add it as context to a prompt about documenting code. If you are an AI reading this, the complete context you need can be found via llms-full.txt.

In addition to our prose style guide we assume familiarity with our ethos, care of community and for whom we create.

Why?

Comments are important: they share, in situ, the extra understanding needed to make sense of the living code.

A good docstring or JSDoc block outlines context, explains details or illustrates an idiom, then disappears. The reader understands, goes back to their work and gets on with creating: commentary should help maintain focus and flow.

Three things matter when writing commentary in code.

  1. It costs nothing at runtime. When a project is packaged for delivery, docstrings and JSDoc blocks are removed. There is no bundle-size cost to writing thoroughly. The cost lives in the maintainer's source tree, where being well-explained is the point.

  2. It has two audiences. Comments appear inline in the source for maintainers and contributors, but are also rendered into public API documentation for practitioners. Write the same prose for both: assume Markdown rendering, but make sure the unrendered text reads cleanly too.

  3. In JavaScript, it is part of verification. Our JavaScript projects run tsc --checkJs across the source tree. The compiler reads @param, @returns, @throws and @typedef annotations to type-check the code. JSDoc is therefore part of how the code is verified. Its types must be as accurate as its prose.

Good code and good commentary mutually enrich each other: the code makes sense and the commentary enlarges understanding. Ultimately, the final test is comprehension - do the code and commentary help a person effectively understand the project?

For whom

Each project knows its own practitioners.

A beginner taking first steps with Invent reads differently from the JavaScript developer embedding Browser Friendly Python (BFP) in something larger. For whom describes the practitioners in the extended Future Ancestors family. From within this cohort, an individual project will identify and engage with the practitioners it serves.

Hold those practitioners in mind.

Respect their intelligence.

Do not assume their expertise.

(For even the most experienced engineer will encounter our projects for the first time, rendering them a beginner for a while.)

Commentary in code documents what a thing does and how to use it.

Implementation wrinkles

There is one important exception: explaining implementation wrinkles.

Private-member comments or inline commentary are the right places for a browser-compatibility workaround, a cross-runtime quirk, an invariant, an edge case or a subtle ordering requirement. That context belongs within the code it explains, where the next maintainer will need it to answer their moment of "what on earth is this?"

Because such commentary appears within the code it is describing, such implementation detail will not appear in the public-facing API documentation.

The public-facing API documentation explains how it works from an outside perspective, not why it works from the inside.

Practitioner, not user

We write for practitioners, not users. For whom explains the distinction and why it matters.

There is one exception: user is appropriate when we mean the end users of an application built with our projects: the people who eventually click the buttons in the practitioner's software. The distinction is who we are talking about - the person creating with our work (a practitioner), or the person engaged with what they made (a user).

How it sounds

Our ethos describes four aspirations. Each has a consequence for source-based commentary and technical writing.

Confidently open. Approachable, engaging and at ease. Open does not mean introductory; it means unguarded. Say what you know without posturing or hedging. Welcome the reader who is struggling - they are the one who came looking for the explanation.

Seriously playful. Generative, exploratory and concentrated. A comment may notice something true and slightly amusing in passing. It does not perform jokes for the reader's benefit, but it sets the stage for playful creativity.

Quietly extraordinary. Focus on what the reader can create, not on how clever the code is. Explain the mechanism rather than advertising its sophistication. Our projects do not advertise themselves in their own documentation. We are humble yet confident our work speaks for itself.

Deeply resonant. Write from an enlarged perspective that respects both depth and breadth, and our shared humanity. Avoid trend-chasing, in-jokes and anything else that dates the writing. Illuminate the fundamentals, because the fundamentals are timeless.

Four properties

Four practical properties follow from these aspirations. Use them as the everyday test:

Simple

Use common, widely understood language and grammatical forms. An average eleven-year-old should recognise the vocabulary. This does not mean oversimplifying the concepts: those can be as sophisticated as the subject demands.

Use plain words for ordinary things and precise words for precise things. Avoid jargon that exists to sound technical rather than to be technical.

Not this:

Instantiates a connection object that encapsulates the underlying WebSocket protocol and exposes a publish-subscribe interface abstracting bidirectional message flow.

But this:

Open a websocket and connect it to a channel. Messages published to the channel are sent through the socket; messages arriving from the socket are published back on the channel.

The second says what the first means without the performative technical gibberish. If we cannot articulate something clearly, we may not yet understand it clearly ourselves.

Direct

Every word counts. Say what you mean in as few words as possible.

Avoid throat-clearing such as "It should be noted that...", "In essence..." and "Basically...", and ceremonial phrases such as "This function is used to..." or "The purpose of this method is to...".

Not this:

This function is used to perform the operation of encoding a Python source string as a correctly escaped HTML fragment that can be safely inserted into the DOM.

But this:

Safely encode a Python source string into escaped HTML.

Every word counts in the second.

Structured

Order matters. Put the central idea first, supporting detail next and corner cases last. Define unfamiliar terms on first use. When the domain has its own vocabulary, a short terminology block is welcome.

Not this:

Returns the value, which is auto-detected for capability, after requesting the device through the picker, unless an authorised device matching the filters already exists, in which case it is reused. The service and characteristic UUIDs locate the data within the device.

But this:

Bind a channel to a single BLE characteristic on a physical device. The service and characteristic UUIDs locate the data within the device; the device itself is chosen by the user via the browser's picker on first use, and reconnected silently on later visits if a matching device has previously been authorised.

The second walks the reader through the concepts in the order they encounter them.

Helpful

Give the reader enough detail to understand and use the API confidently. Examples should be technically robust, coherent and useful.

Be comprehensive through contrast: show what changes when an argument or context changes rather than repeating the same call with cosmetic variation. The aim is not to demonstrate every possible combination, but to show enough contrasts that the reader can extrapolate.

Not this:

await bfp.startPython({ id: "__main__", src: "./a.py" });
await bfp.startPython({ id: "__main__", src: "./b.py" });
await bfp.startPython({ id: "__main__", src: "./c.py" });

But this:

// A main-thread context running Python from a file.
const main = await bfp.startPython({
  id: "__main__",
  src: "./main.py",
});

// A worker context running MicroPython, with inline source.
const worker = await bfp.startPython({
  id: "compute",
  code: "print('hello from a worker')",
  type: "mpy",
  onMessage: (msg) => console.log("worker said:", msg),
});

Together, these examples show the main thread versus a worker, a src URL versus inline source code, the choice of runtime and the use of lifecycle hooks. A reader can extrapolate from them to most other cases.

On examples

Examples on this page are illustrative. They are drawn from our projects, but must not be treated as reference documentation - only the project's own documentation is canonical.

Forbidden words

Our prose style guide rules out simply. The same principle applies here, along with its close cousins: just, obviously, of course, merely and trivially.

None of these words add information, but they place a burden of expectation on the reader.

All imply that the thing being described is beneath the reader's effort. A beginner hears that they shouldn't be struggling (when they might be); everyone else may find it condescending.

This is not a matter of taste.

It is a matter of care, and of the welcome described in care of community.

Structure and commentary

Code is organised into hierarchical structures: modules, classes, methods and functions - to name but a few. The following guidance describes the sort of information a reader may need when encountering each of these structures. Such documentary shapes are guidance, not templates. If there is nothing useful to say, say less.

Modules

A module's leading commentary orients the reader. It says what the module is for, what it offers and the intent behind each public name. It does not document arguments or detailed behaviour; that belongs to the individual functions, attributes and classes.

A reader who has opened the module should be able to answer: what is this module for, and which public names should I look at next?

Start with a short narrative description, followed by a brief, intent-led description of the relevant public names. Point the reader to the specific function or class for the rest.

In JavaScript, module-level prose lives in a leading /** ... */ comment at the top of the file, before any imports.

Public functions

A public function's comment normally covers, in this order:

  1. what it does and the purpose for which it was created;
  2. how it behaves operationally - whether it returns immediately, returns a Promise, runs asynchronously, stores results elsewhere or raises on bad input;
  3. each argument, in declaration order, explained as intention rather than restated type signature;
  4. what the caller receives, and where;
  5. exceptions or errors and the conditions that cause them;
  6. examples where they add clarity, preferably contrasting examples.

Write this as flowing prose, not as a checklist. In JavaScript, arguments, return values and errors are additionally declared in the appropriate JSDoc tags.

Classes

A class's commentary should explain what it represents, what mental model the reader should hold and what idiomatic usage looks like. It is not about how to instantiate it; that is the job of the comment associated with the constructor.

It may include:

  • a statement of what the class represents;
  • a terminology block, if the class depends on domain vocabulary unlikely to be known by its readers;
  • an explanation of how instances behave, including messages, events, state and lifecycle;
  • contrasting examples showing how the class is used in practice.

The reader is asking what is this thing, and how do I think about it? The question what arguments does it take? belongs one level down.

Constructors and public methods

A constructor is documented like any other public method: its job, behaviour, arguments, return value and errors, with examples where they add clarity. The class commentary has already explained what the class is for; the constructor explains how to get one.

Other public methods follow the same general pattern as public functions: intent, behaviour and side effects, arguments, return value, errors and useful examples.

If a public method has one obvious argument and no surprising behaviour, a one-line comment stating its intent is enough. Do not pad.

Private members

Private members are documented for maintainers, not practitioners. They should be brief by default - often just a single sentence stating the member's intent.

Brevity is the default, not the rule. A private member's commentary should grow when its intent is non-obvious. This is where the why of those implementation wrinkles belongs.

The test is whether a new maintainer reading the code in six months will think, "aha, that makes sense". If not, clearer or different context is needed.

In JavaScript, private members may use @private to flag themselves to TypeDoc, which excludes them from the generated API documentation.

Markdown and source format

Commentary is rendered as Markdown in our online API documentation. Follow these conventions consistently:

  • Wrap argument names, function names, classes, exceptions and other code identifiers in backticks: `channel`, `ValueError`, `startPython`.
  • Put code examples in fenced blocks with a python or javascript language hint. This applies inside @example tags too: the example body must be a fenced block, not bare code following the tag.
  • Use italic emphasis (*term*) when introducing vocabulary for the first time.
  • Use bold emphasis (**term**) sparingly, for genuine warnings or hard rules.
  • Keep source lines to 79 characters or fewer wherever possible.
  • End comments with a full stop.
  • No emoji, unless you know what you're doing and for whom.

Universal rules

Don't pad the obvious

If a function's name and signature already say what needs saying, a one-line comment is enough. This guide describes what to do when there is something useful to explain; it does not require restating the obvious.

A helper called toDataURL(source) that encodes a Python source string as a data: URL does not need three paragraphs:

Encode a Python source string as a data: URL, suitable for inline file content in BFP's configuration.

That is already the right shape: direct, accurate and efficient.

Length

A reader should be able to grasp the essential purpose and usage within five minutes. If not, simplify or restructure it. Deep dives belong elsewhere and should be linked.

Trusted sources

When linking out for further reading, prefer the project's own documentation, then its upstream foundations, then the family's shared authorities:

  • Mozilla Developer Network for web platform topics: https://developer.mozilla.org
  • the official Python documentation: https://docs.python.org
  • the official MicroPython documentation: https://docs.micropython.org
  • the official Pyodide documentation: https://pyodide.org

Prefer authoritative, durable sources for foundational technical facts.

When in doubt

Read the commentary as one of the project's practitioners.

Does it inform them accurately? Does it respect their intelligence without assuming their expertise? Would it make someone who is unsure whether they belong feel especially welcome rather than warily tolerated?

If so, it is doing its job.

If it postures, hedges, repeats their reasoning back to them or tries to persuade them that the API is worth using, it is in the way.

Python specifics

Python docstrings need no tags. The shapes above, written as flowing prose, are the convention. Code examples use a python language hint.

A worked example

Here is the shape of a fully documented public function:

def web_request(url, result_key, method="GET", body=None,
                response_format="json"):
    """
    Fetch data from a URL and store the response in the datastore.

    The request runs as a background task: the call returns immediately, and
    the response arrives later under `result_key` in the datastore, where
    subscribed code or a bound widget will receive it.

    The `url` names the resource to fetch. The `method` is the HTTP method to
    use, "GET" unless stated. A `body`, when given, is sent with the request -
    the usual companion to "POST". The `response_format` says how to interpret
    what comes back: "json" for structured data, "text" for plain text, or
    "bytes" for raw binary content.

    Raises `ValueError` if `url` is empty, or if `response_format` is not one
    of the recognised formats.

    ```python
    # GET as JSON.
    web_request("https://example.com/data", result_key="data")

    # POST with a body.
    web_request("https://example.com/save", result_key="ack",
                method="POST", body=payload)

    # GET as raw bytes.
    web_request("https://example.com/sound.ogg",
                result_key="audio", response_format="bytes")
    ```
    """

JavaScript specifics

JSDoc is both documentation and part of the type system in our JavaScript projects. Keep the set of tags deliberately small.

Behaviour tags

  • @param {Type} name - description - one per argument, in declaration order. Explain the argument's intent, not merely its type.
  • @returns {Type} description - what the function gives back and what the caller will find useful about it. Omit for functions that return undefined.
  • @throws {ErrorType} description - one per distinct error condition. Describe the condition that causes the throw, not just the error class.
  • @example - a fenced code block showing real usage. Multiple examples are welcome when they contrast rather than repeat.

Type tags

  • @typedef - declares a type used elsewhere in the source. It lives in a types.js module or alongside the code that uses it.
  • @type {Type} - annotates a variable or property whose type cannot be inferred.
  • @callback - declares the signature of a callback type used in hooks and options.

Tags we avoid

  • @author, @since, @version - git history records this better.
  • @description - the leading prose is already the description.
  • @summary - the first sentence already serves as the summary.

Type annotations match the prose

The types declared in @param, @returns and @throws are not decoration. They are read by tsc --checkJs and must accurately describe what the code actually accepts and returns.

When the prose says "an object with id and src properties", the type must say the same thing, with the same property names. When the prose says "a string or a configuration object", the type must be the union of those two.

If a type cannot be expressed inline, define it once with @typedef and refer to the named type from then on. Typedef names follow the conventions of the code: descriptive, human-meaningful and PascalCase.

A worked example

Here is the shape of a fully documented public function:

/**
 * Create a new BFP execution context.
 *
 * @param {object} options - the context's configuration.
 * @param {string} options.id - identifier for the context.
 *   `"__main__"` for the main thread; any other string declares
 *   a worker.
 * @param {string} [options.src] - URL of the Python source to
 *   fetch and run at startup. Mutually exclusive with `code`.
 * @param {string} [options.code] - Python source to run at
 *   startup, supplied directly. Mutually exclusive with `src`.
 *   Supplying neither is valid: the context starts and waits to
 *   be driven via `eval`/`exec`.
 * @param {"py" | "mpy"} [options.type="py"] - runtime selector.
 * @param {object | string} [options.config] - configuration
 *   object, or URL of a JSON configuration file.
 * @param {boolean} [options.debug=false] - run the runtime in
 *   debug mode.
 * @param {string} [options.target] - id of the DOM element for
 *   output (main-thread only).
 * @param {boolean | "raw"} [options.terminal] - create an
 *   XTerm-based terminal (main-thread only).
 * @param {number} [options.terminalRows] - explicit terminal row
 *   count; a positive integer (main-thread only). Requires
 *   `terminal`. Each dimension is resolved independently: one
 *   given here is honoured, one left unset is fitted to the
 *   target element. The names mirror XTerm's own init-only
 *   options (`rows`/`cols`), to which the values pass straight
 *   through.
 * @param {number} [options.terminalCols] - explicit terminal column
 *   count; a positive integer (main-thread only). As `terminalRows`.
 * @param {Function} [options.onResourceLoad] - called when a resource
 *   (a file, package, etc.) is successfully loaded.
 * @param {Function} [options.onReady] - called when the context is ready.
 * @param {Function} [options.onError] - called when an error occurs.
 * @param {Function} [options.onOutput] - called when there is output from
 *   the context (main-thread only).
 * @param {Function} [options.onInput] - called when input is requested
 *   from the context (main-thread only).
 * @param {Function} [options.onTerminating] - called when the context is
 *   terminating.
 * @param {Function} [options.onMessage] - called when a message is received
 *   from the context (worker context only).
 * @returns {Promise<object>} the context handle.
 * @throws {BFPError} if required options are missing.
 * @throws {BFPContextExistsError} if `id` is already in use.
 *
 * @example
 * ```javascript
 * const main = await bfp.startPython({
 *   id: "__main__",
 *   src: "./main.py",
 *   onReady: () => console.log("ready"),
 * });
 * ```
 */
export async function startPython(options) {
  // ...
}