Theming and other customizations

Configure sidebar expanding/collapsing

By default, the sidebar only lists one level of objects (always expanded), to allow objects to expand/collapse and show first nested content, use the following option:


This value describe how many nested modules and classes should be expandable.


Careful, a value higher than 1 (which is the default) can make your HTML files significantly larger if you have many modules or classes.

To disable completely the sidebar, use option --no-sidebar


Currently, there are 2 main themes packaged with pydoctor: classic and readthedocs.

Choose your theme with option:



Additionnaly, the base theme can be used as a base for customizations.

Tweak HTML templates

They are 3 special files designed to be included in specific places of each pages.

  • header.html: at the very beginning of the body

  • subheader.html: after the main header, before the page title

  • extra.css: extra CSS sheet for layout customization

To include a file, write your custom HTML or CSS files to a directory and use the following option:


If you want more customization, you can override the default templates in pydoctor/themes/base with the same method.

HTML templates have their own versioning system and warnings will be triggered when an outdated custom template is used.

Demo theme example

There is a demo template inspired by Twisted web page for which the source code is here. You can try the result by checking this page.


This example is using the base theme.

Override objects privacy (show/hide)

Pydoctor supports 3 types of privacy. Below is the description of each type and the default association:

  • PRIVATE: By default for objects whose name starts with an underscore and are not a dunder method. Rendered in HTML, but hidden via CSS by default.

  • PUBLIC: By default everything else that is not private. Always rendered and visible in HTML.

  • HIDDEN: Nothing is hidden by default. Not rendered at all and no links can be created to hidden objects. Not present in the search index nor the intersphinx inventory. Basically excluded from API documentation. If a module/package/class is hidden, then all it’s members are hidden as well.

When the default rules regarding privacy doesn’t fit your use case, use the --privacy command line option. It can be used multiple times to define multiple privacy rules:


where <PRIVACY> can be one of PUBLIC, PRIVATE or HIDDEN (case insensitive), and <PATTERN> is fnmatch-like pattern matching objects fullName.

Privacy tweak examples

  • --privacy="PUBLIC:**" Makes everything public.

  • --privacy="HIDDEN:twisted.test.*" --privacy="PUBLIC:twisted.test.proto_helpers" Makes everything under twisted.test hidden except twisted.test.proto_helpers, which will be public.

  • --privacy="PRIVATE:**.__*__" --privacy="PUBLIC:**.__init__" Makes all dunder methods private except __init__.


The order of arguments matters. Pattern added last have priority over a pattern added before, but an exact match wins over a fnmatch.


See pydoctor.qnmatch for more informations regarding the pattern syntax.


Quotation marks should be added around each rule to avoid shell expansions. Unless the arguments are passed directly to pydoctor, like in Sphinx’s, in this case you must not quote the privacy rules.

Use a custom system class

You can subclass the pydoctor.model.System and pass your custom class dotted name with the following argument:


System class allows you to customize certain aspect of the system and configure the enabled extensions. If what you want to achieve has something to do with the state of some objects in the Documentable tree, it’s very likely that you can do it without the need to override any system method, by using the extension mechanism described below.

Brief on pydoctor extensions

The AST builder can now be customized with extension modules. This is how we handle Zope Interfaces declarations and twisted.python.deprecate warnings.

Each pydocotor extension is a Python module with at least a setup_pydoctor_extension() function. This function is called at initialization of the system with one argument, the pydoctor.extensions.ExtRegistrar object representing the system.

An extension can register multiple kind of components:

Take a look at built-in extensions pydoctor.extensions.zopeinterface and pydoctor.extensions.deprecate. Navigate to the source code for a better overview.

A concrete example

Let’s say you want to write a extension for simple pydantic classes like this one:

from typing import ClassVar
from pydantic import BaseModel
class Model(BaseModel):
    a: int
    b: int = Field(...)
    name:str = 'Jane Doe'
    kind:ClassVar = 'person'

First, we need to create a new module that will hold our extension code: mylib._pydoctor. This module will contain visitor code that visits ast.AnnAssign nodes after the main visitor. It will check if the current context object is a class derived from pydantic.BaseModel and transform each class variable into instance variables accordingly.

# Module mylib._pydoctor

import ast
from pydoctor import astutils, extensions, model

class PydanticModVisitor(extensions.ModuleVisitorExt):

    def depart_AnnAssign(self, node: ast.AnnAssign) -> None:
        Called after an annotated assignment definition is visited.
        ctx = self.visitor.builder.current
        if not isinstance(ctx, model.Class):
            # check if the current context object is a class

        if not any(ctx.expandName(b) == 'pydantic.BaseModel' for b in ctx.bases):
            # check if the current context object if a class derived from ``pydantic.BaseModel``

        dottedname = astutils.node2dottedname(
        if not dottedname or len(dottedname)!=1:
            # check if the assignment is a simple name, otherwise ignore it

        # Get the attribute from current context
        attr = ctx.contents[dottedname[0]]

        assert isinstance(attr, model.Attribute)

        # All class variables that are not annotated with ClassVar will be transformed to instance variables.
        if astutils.is_using_typing_classvar(attr.annotation, attr):

        if attr.kind == model.DocumentableKind.CLASS_VARIABLE:
            attr.kind = model.DocumentableKind.INSTANCE_VARIABLE

def setup_pydoctor_extension(r:extensions.ExtRegistrar) -> None:

class PydanticSystem(model.System):
    # Declare that this system should load this additional extension
    custom_extensions = ['mylib._pydoctor']

Then, we would pass our custom class dotted name with the argument --system-class:


Et voilà.

If this extension mechanism doesn’t support the tweak you want, you can consider overriding some pydoctor.model.System methods. For instance, overriding pydoctor.model.System.__init__() method could be useful, if some want to write a custom pydoctor.sphinx.SphinxInventory.


If you feel like other users of the community might benefit from your extension as well, please don’t hesitate to open a pull request adding your extension module to the package pydoctor.extensions.

Use a custom writer class

You can subclass the pydoctor.templatewriter.TemplateWriter (or the abstract super class pydoctor.templatewriter.IWriter) and pass your custom class dotted name with the following argument:


The option is actually badly named because, theorically one could write a subclass of pydoctor.templatewriter.IWriter (to be used alongside option --template-dir) that would output Markdown, reStructuredText or JSON.


Pydoctor does not have a stable API yet. Code customization is prone to break in future versions.