LeRobot documentation
Writing docstrings
Writing docstrings
LeRobot’s API reference is generated directly from the docstrings in src/lerobot/. A docstring is not a
comment — it is the published documentation for that object, and the format below is what the renderer and
the CI checks parse.
This page is the contract. If you are adding or editing anything public in src/lerobot/, follow it.
An undocumented public method is an invisible one.
[[autodoc]]silently skips members that have no docstring — no warning, no error, it simply does not appear on the rendered page. Coverage and API-reference completeness are the same problem.
The format in one example
Google section headers, Hugging Face type formatting. Both, not one or the other.
def send_action(self, action: RobotAction, rate_hz: float = 30.0) -> RobotAction:
"""Command the robot to move to a target joint configuration.
Values are clipped by the configured maximum relative target before reaching the motors, so the
returned action may differ from the requested one.
Args:
action (`dict[str, float]`):
Target values keyed by motor name, e.g. `{"shoulder_pan.pos": 0.0}`. Keys must match the
robot's action features.
rate_hz (`float`, *optional*, defaults to `30.0`):
Control loop frequency.
Returns:
`dict[str, float]`: The action actually written to the motors after safety clipping.
Raises:
DeviceNotConnectedError: If the robot has not been connected.
Example:
```python
>>> from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
>>> robot = SO101Follower(SO101FollowerConfig(port="/dev/ttyACM0")) # doctest: +SKIP
>>> robot.connect() # doctest: +SKIP
>>> robot.send_action({"shoulder_pan.pos": 0.0}) # doctest: +SKIP
```
"""Cross-references are omitted from the examples on this page — see Cross-references for their syntax and why they cannot be shown inside a code block.
Rules
Sections
Args: · Returns: · Raises: · Yields: · Example: · Note:
In that order. No other section headers. A one-line summary comes first, then an optional free-form description, then the sections.
The Args: line is machine-parsed
name (`type`, *optional*, defaults to `X`):
Description, indented on its own line.The *optional*, defaults to clause is checked against the real signature default by make check-docstrings. It is not decorative — if you write a default that has drifted from the code, CI
fails. Omit the clause entirely for required parameters:
Args:
port (`str`):
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
max_relative_target (`float | dict[str, float]`, *optional*):
Caps the magnitude of the relative positional target vector. `None` disables clipping.
use_degrees (`bool`, *optional*, defaults to `True`):
Keep `True` for backward compatibility with existing policies and datasets.Types go in backticks. Use *optional* with no defaults to when the default is None or is otherwise not
worth restating.
Returns: is type-first
One indented line, type first, then a colon, then the description:
Returns:
`dict[str, float]`: The action actually written to the motors after safety clipping.Yields: takes the same shape.
**Attributes**: , never Attributes:
doc-builder parses a bare Attributes: as a synonym for Parameters:, so your attributes get rendered
as constructor arguments. This is silent and wrong. Whenever the attributes differ from the constructor
parameters, use the bold form with a -- separator:
class Robot(abc.ABC):
"""The base abstract class for all LeRobot-compatible robots.
**Attributes**:
- **config_class** (`type[RobotConfig]`) -- The expected configuration class for this robot.
- **name** (`str`) -- The unique robot name used to identify this robot type.
"""Note --, not :.
Cross-references
Use doc-builder’s bracket syntax: a square-bracketed backtick-quoted path. Sphinx roles (:pymeth:, :pyattr:) are not supported and render as literal text on the page.
| Want | Write |
|---|---|
| Class in the main package | [Robot] |
| Method, show the full path | [Robot.connect] |
| Method, show the bare name | [~Robot.connect] |
| Nested path | [~robots.Robot.connect] |
| Object in another HF library | [~accelerate.Accelerator] |
The ~ strips the path from the link text only; the link still resolves to the full path.
doc-builder resolves this syntax everywhere in a page — including inside fenced code blocks. That is why the docstring examples on this page use plain prose instead of cross-references: a code block containing one would render the resolved link rather than the syntax you need to type. In your own docstrings, use cross-references freely; this restriction only affects documentation about the syntax.
Callouts
Use GitHub-style blockquotes:
> [!TIP]
> Call this once at startup — it takes about two seconds.
> [!WARNING]
> Torque is disabled on disconnect. The arm will drop if it is holding a load.The <Tip> component is legacy per doc-builder; don’t add new ones.
Examples must be fenced
An example lives inside a fenced ```python block containing >>>. The fence is what makes it render
as a code block, and it is what the doctest preprocessor’s regex looks for:
Example:
```python
>>> from lerobot.robots.so_follower import SO101FollowerConfig
>>> cfg = SO101FollowerConfig(port="/dev/ttyACM0")
>>> cfg.use_degrees
True
```An unfenced
>>>is still collected — doctest finds prompts anywhere in a docstring. What you lose is the rendering, so it shows up as a wall of prose on the page. Every example needs the fence.
Every example either executes in CI or carries # doctest: +SKIP. Anything that touches hardware, a GPU, or
downloads from the Hub gets +SKIP:
Example:
```python
>>> robot.connect() # doctest: +SKIP
>>> policy = ACTPolicy.from_pretrained("lerobot/act_aloha_sim_transfer_cube_human") # doctest: +SKIP
```Add files containing runnable examples to utils/documentation_tests.txt.
Put examples on the three to five genuine entry points of a module. Examples on trivial accessors are noise.
Three patterns you will hit constantly
Config dataclasses
Configuration fields are historically documented with # comments above each field. doc-builder cannot
see inline comments — such a class renders with every field listed and not a single description. Move them
into an Args: block on the class docstring:
@dataclass
class SOFollowerConfig:
"""Configuration for SO-family follower arms.
Args:
port (`str`):
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
max_relative_target (`float | dict[str, float]`, *optional*):
Caps the magnitude of the relative positional target vector. A scalar applies to all motors;
a dict maps motor name to a per-motor cap. `None` disables clipping.
use_degrees (`bool`, *optional*, defaults to `True`):
Keep `True` for backward compatibility with existing policies and datasets.
"""
port: str
max_relative_target: float | dict[str, float] | None = None
use_degrees: bool = Truedoc-builder does not inherit docstrings from base classes. LeRobot’s registered config classes are often thin multiple-inheritance shims:
@RobotConfig.register_subclass("so101_follower") @dataclass class SOFollowerRobotConfig(RobotConfig, SOFollowerConfig): passThat class renders every field — including the ones it inherits — with no descriptions at all, no matter how well the bases are documented. The
Args:block must live on the concrete class that[[autodoc]]names, and it must cover inherited fields too.
Base class, then concrete subclass
The abstract base carries the canonical contract. Subclasses document only what deviates — port semantics, calibration quirks, motor layout, supported feature keys. Do not copy the base contract into every subclass.
Robot, Teleoperator, Camera, MotorsBus, ProcessorStep, and PreTrainedPolicy all follow this
shape.
Module-level aliases
Several public names are aliases rather than distinct classes:
SO100FollowerConfig = SOFollowerRobotConfig SO101FollowerConfig = SOFollowerRobotConfig
[[autodoc]] resolves the alias and renders the canonical class name, so a ## SO101FollowerConfig heading will show class lerobot.robots.so_follower.SOFollowerRobotConfig in the body. Document the
canonical class once, and mention the aliases in the page’s prose rather than giving each alias its own
autodoc block.
What not to document
- Private members. Anything starting with
_is not part of the public API. - The type annotation restated as prose.
port (str): A string.adds nothing. Say what it is for. - Vendored upstream code.
src/lerobot/policies/molmoact2/molmoact2_hf_model/is vendored fromtransformersand already carries upstream-style docstrings. Leave it alone — restyling it only creates conflicts on the next sync. It is excluded from the API reference and from the docstring checks.
How this is enforced
| Check | What it catches |
|---|---|
make check-docstrings | An Args: entry that doesn’t match the signature; a documented default that has drifted from the real one |
make doctest | Examples that no longer run |
make check-doctest-list | Stale or unsorted entries in utils/documentation_tests.txt |
ruff (D rules) | Google-convention style violations |
interrogate | Docstring coverage falling below the current threshold |
| doc-builder | A [[autodoc]] path that points at something that doesn’t exist — this breaks the docs build |
Run them together before opening a PR:
make check-docstrings && make doctest && pre-commit run --all-files
Then render the page and actually look at it:
doc-builder build lerobot docs/source/ --build_dir /tmp/doc-build
Checklist
- Every public member you touched has a docstring.
- Every
Args:entry matches the signature, including the*optional*, defaults toclause. -
Returns:is type-first on one indented line. - No bare
Attributes:— use**Attributes**:with--separators. - No Sphinx roles — cross-references use [
~module.Class.method]. - Examples are inside a fenced
```pythonblock, and either run in CI or carry# doctest: +SKIP. - Config dataclass fields are in an
Args:block on the concrete class, not#comments. - The rendered page has been eyeballed.