Sphinx Extension Learning

documentation
code
Published

August 21, 2026

This article is an aggregation of my notes about writing a Sphinx extension.

Definitions

  • builder: A class (inheriting from Builder) that takes parsed documents and performs an action on them. Normally, builders translate the documents to an output format, but it is also possible to use builders that check for broken links or coverage information.

  • directive: A rStructuredText markup element that allows marking a block of content with special meaning. Directes are supplied not only by docutils, but Sphinx and custom extension can add their own. The basic directive syntax looks like this:

    .. directive-name:: argument ...
      :option-name: option-value
    
      Content of the directive.
  • domain: A domain is a collection of markup (reStructuredText directives and roles) to describe and link to objects belonging together, e.g. elements of a programming language. Directive and role names in a domain have names like domain:name, e.g. py:function. Having domains means that there are no namin problems when one set of documentation wants to refer to e.g. C++ and Python classes. It also means that extensions that support the documentation of whole new languages are much easier to write.

  • environment: A structure where information about all documents under the root is saved, and used for cross-referencing. The environment is pickled after the parsing stage, so that successive runs only need to read and parse new and changed documents.

  • role: A reStructuredText markup element that allows marking a piece of text. Like directives, roles are extensible. The basic syntax looks like this:

    :rolename:`content`

Linting Sphinx in VS Code

Configure VS Code for Sphinx-aware linting

For a project with custom Sphinx directives/roles, the right setup is: - Use Esbonio, not the generic reStructuredText linters. - Point Esbonio at the same Python environment that runs your Sphinx build. - Keep the generic linters disabled for this project.

My project already registers custom extensions in conf.py, and the directive is implemented in custom.py. That is exactly the kind of setup plain RST lint tools cannot understand.

Install the right extensions

  • Install the VS Code extension: swyddfa.esbonio
  • Keep the Python extension enabled so Esbonio can locate the interpreter

Why this is the correct fix

Generic linters parse raw reStructuredText and only know built-in docutils directives. They do not load your Sphinx app and therefore cannot understand custom entries such as:

  • .. todo::
  • .. somecustomdirective::
  • roles added with app.add_role(...)

Esbonio does run a Sphinx process and loads the project config, so it sees the registration from conf.py and the extension logic in custom.py. That is why it can validate custom directives and roles correctly.


Important rule

Do not use ignore-directives for custom Sphinx directives if your goal is correct linting.
Instead: - register the directive in Sphinx, - point Esbonio to the project Python environment, - disable the generic doc8/rstcheck/rst-lint linters for this workspace.

This is the clean Sphinx-native approach.

Notes on building a Sphinx extension

  • Creating a custom builder is likely a great idea to lint/check source code documentation. Inputs, Outputs, examples, and other things could be checked for consistency and correctness.
  • Tie into the event system is key. Need to dig in here.
  • docutils is the base library for Sphinx, so it is important to understand the docutils vocubulary. Sphinx is based on this vocabulary, but adds its own concepts (like builders, domains, and environments).
  • I don’t get configuring builder yet. https://www.sphinx-doc.org/en/master/development/howtos/builders.html
  • Any defined config values should begin with the plugin name. This avoids collisions with other extensions. For example, if your extension is called myext, then config values should be named like myext_somevalue.