.. seealso:: This page is the source of truth for the ``isaaclab-following-coding-style``, ``isaaclab-preparing-pr-workflow``, and ``isaaclab-writing-changelog-fragments`` agent skills (`skills/developer/coding-style/ <../../../skills/developer/coding-style/SKILL.md>`__, `skills/developer/pr-workflow/ <../../../skills/developer/pr-workflow/SKILL.md>`__, `skills/developer/changelog-fragments/ <../../../skills/developer/changelog-fragments/SKILL.md>`__). When shared guidance changes, update affected skill workflows and examples without copying the rules. See :doc:`/source/developer-tools/agent_skills`.
We wholeheartedly welcome contributions to the project to make the framework more mature and useful for everyone. These may happen in forms of:
- Bug reports: Please report any bugs you find in the issue tracker.
- Feature requests: Please suggest new features you would like to see in the discussions.
- Code contributions: Please submit a pull request.
- Bug fixes
- New features
- Documentation improvements
- Tutorials and tutorial improvements
We prefer GitHub discussions for discussing ideas, asking questions, conversations and requests for new features.
Please use the issue tracker only to track executable pieces of work with a definite scope and a clear deliverable. These can be fixing bugs, new features, or general updates.
Attention!
Please refer to the Google Style Guide for the coding style before contributing to the codebase. In the coding style section, we outline the specific deviations from the style guide that we follow in the codebase.
We use GitHub for code hosting. Please follow the following steps to contribute code:
- Create an issue in the issue tracker to discuss the changes or additions you would like to make. This helps us to avoid duplicate work and to make sure that the changes are aligned with the roadmap of the project.
- Fork the repository.
- Create a new branch for your changes.
- Make your changes and commit them.
- Push your changes to your fork.
- Submit a pull request to the develop branch.
- Ensure all the checks on the pull request template are performed.
After sending a pull request, the maintainers will review your code and provide feedback.
Ensure that your code is formatted and documented, and run the relevant tests and required CI checks as described in Unit Testing and Tools.
Tip
It is important to keep the pull request as small as possible. This makes it easier for the maintainers to review your code. If you are making multiple changes, please send multiple pull requests. Large pull requests are difficult to review and may take a long time to merge.
More details on the code style and testing can be found in the Coding Style and Unit Testing sections.
Read the contribution sections and skills that apply to the current task. Reuse guidance already loaded in the conversation while it remains current; reread when relevant files change or needed context is lost. Native skill discovery already supplies descriptions, so do not load the whole catalog or every linked reference. Load PR preparation guidance when preparing the final change.
Before editing, identify the behavior's owner, the closest reusable implementation, and the smallest change that fixes the problem. For changes that span packages or add work to a hot path, briefly state the affected boundaries and runtime cost. Distinguish requested scope changes from unrelated improvements and record the latter as follow-ups.
Use bounded searches and read relevant functions and callers instead of dumping unrelated files or tool inventories. Batch independent reads, keeping their combined output small enough to inspect. When delegation is requested or an applicable workflow calls for it, give each agent a bounded question and clear file ownership; serialize edits to shared infrastructure.
Use a separate worktree when the current checkout has unrelated changes. Run commands from the worktree and give it its own uv environment; uv can reuse downloaded packages from its cache. If an existing environment must be reused, verify the interpreter and imported source paths before validation. The CLI resolves its repository root from the imported package, not the shell's working directory. Check that root with:
uv run python -c "import sys; from isaaclab.paths import ISAACLAB_ROOT; print(sys.executable); print(ISAACLAB_ROOT)"Also verify the imported locations of packages touched by the change. Avoid concurrent environment synchronization or documentation builds against the same environment or output directory.
Run the narrowest relevant checks during editing, then the required final checks. Record the tested revision or relevant diff, command, and result in the task notes. Reuse successful results until a relevant source, dependency, configuration, or test change invalidates them. Diagnose failed checks before retrying; keep environmental failures distinct from regressions introduced by the change.
Launch long-running checks once and retain their process or CI run IDs and logs. Use a local watch command or longer polling intervals while continuing independent work, and report meaningful state changes rather than repeatedly reading unchanged logs. If the request only requires starting CI, hand off the run link after confirming it started. Await completion when the result is required.
Contributing to the documentation is as easy as contributing to the codebase. All the source files
for the documentation are located in the IsaacLab/docs directory. The documentation is written in
reStructuredText format.
We use Sphinx with the Book Theme for maintaining the documentation.
API [source] links open the implementation on GitHub, using the published documentation's
branch or tag (develop for a current-checkout build by default). We do not generate local
Python source pages; viewing the implementation requires internet access.
Sending a pull request for the documentation is the same as sending a pull request for the codebase. Please follow the steps mentioned in the Contributing Code section.
For documentation media, only small .jpg files should be committed directly to the Isaac Lab repository.
Upload larger images, videos, animations, and other media to an external hosting location, then link to or embed
the externally hosted file from the documentation.
Caution!
Install uv before building
the documentation. The build command syncs the dev extra, which includes
documentation requirements, into the repository's .venv.
Choose documentation validation according to the changed behavior:
- Skip Sphinx when rendered docs are unaffected, including standalone
AGENTS.mdandskills/edits. Run the relevant code or skill checks instead. - Use the incremental HTML preview while editing documentation pages.
- Use a clean build for API signatures or docstrings, Sphinx configuration or extensions, and theme changes. Sphinx's cache does not reliably detect Python source changes.
- Run one clean, warning-free build of final documentation-affecting changes before submitting a PR. Repeat it only after further relevant changes or a failure. CI also runs the clean build.
For an incremental HTML preview, run this command from the repository root on Linux or Windows:
uv run --extra dev --directory docs python -m sphinx -W --keep-going -j auto . _build/incrementalOn systems with Make, the equivalent command is:
uv run --extra dev make -C docs incremental-docsOpen docs/_build/incremental/index.html to inspect the preview. Subsequent runs reuse the cache
and treat new warnings as errors, but can omit old warnings and retain deleted pages. Use a clean
build for final validation, and avoid concurrent builds in the same output directory.
For the final clean build, run the following command from the repository root. It installs the documentation packages and builds the current version:
uv run --extra dev isaaclab --docsThe documentation is generated in docs/_build/current. Open
docs/_build/current/index.html in a browser to view it. Each build clears the current
HTML output and its Sphinx cache so deleted pages and cached warnings cannot be carried over.
Large asset files should not be added directly to the Isaac Lab repository. Instead, host them in a separate repository and link to them from the relevant documentation.
Please checkout the Isaac Sim Assets for more information on what is presently available.
Attention!
We are currently working on a better way to contribute assets. We will update this section once we have a solution. In the meantime, please follow the steps mentioned below.
To host your own assets, the current solution is:
- Create a separate repository for the assets and add it over there
- Make sure the assets are licensed for use and distribution
- Include images of the assets in the README file of the repository
- Send a pull request with a link to the repository
We will then verify the assets and their licensing and determine how to integrate them. If you have questions, please open an issue in the repository.
Each release-managed package maintains a changelog in docs/CHANGELOG.rst and its version in
pyproject.toml.
The changelog contains the curated, chronologically ordered list of notable changes for each package version.
Note
CHANGELOG.rst and the package version in pyproject.toml are compiled nightly by CI from
per-PR towncrier fragment files — contributors do not
edit them directly. For every package your PR touches in source/<pkg>/ (outside
changelog.d/), add fragments under source/<pkg>/changelog.d/:
<slug>.<type>.rst— one file per entry type, where<type>isadded,changed,deprecated,removedorfixed; the file holds that section's bullets.<slug>.minoror<slug>.major— an empty file that raises the version bump from patch to minor (new public API) or major (breaking change).<slug>.skip— an empty file for no entry and no bump (CI / docs / test-only PRs).
<slug> is any short, unique name; your branch name with / replaced by -
is the recommended default. Within a batch the highest tier wins for the package.
The package version in pyproject.toml is bumped by CI according to
Semantic Versioning.
The changelog file is written in reStructuredText format. The goal of this changelog is to help users and contributors see precisely what notable changes have been made between each release of a package. This is a MUST for every release-managed package.
For each fragment, please follow the following guidelines:
- Each fragment's
<type>names the changelog section its bullets appear under.added: For new features.changed: For changes in existing functionality.deprecated: For soon-to-be removed features.removed: For now removed features.fixed: For any bug fixes.
- Each change is described with a
*bullet point; continuation lines are indented. - Prefix breaking changes with Breaking: and provide migration guidance for deprecated, changed, or removed behavior that requires callers to adapt.
- The bullet points are written in the past tense.
- This means that the change is described as if it has already happened.
- The bullet points should be concise and to the point. They should not be verbose.
- The bullet point should also include the reason for the change, if applicable.
Tip
When in doubt, please check the style in the existing changelog files and follow the same style.
For example, source/isaaclab/changelog.d/fix-partial-reset.fixed.rst:
* Fixed contact sensor reset behavior when only a subset of environments was reset.Validate against the PR's base without creating temporary remote-tracking refs:
uv run python tools/changelog/cli.py check upstream/develop --include-worktreeThe checker accepts remote-qualified refs, full refs, and commit SHAs. Branch shorthand such as
develop continues to prefer origin/develop when it exists; use refs/heads/develop to
select a local branch explicitly. The pre-commit hook uses ISAACLAB_CHANGELOG_BASE_REF when
set, otherwise develop. Fetch the intended base before validation.
We follow the Google Style Guides for the codebase. For Python code, the PEP guidelines are followed. Most important ones are PEP-8 for code comments and layout, PEP-484 and PEP-585 for type-hinting.
For documentation, we adopt the Google Style Guide for docstrings. We use Sphinx for generating the documentation. Please make sure that your code is well-documented and follows the guidelines.
Make the smallest change that solves the problem. Read surrounding code, callers, tests, and documentation before changing an interface. Apply these rules when adding code or cleaning up existing implementations:
- Preserve observable behavior unless the change explicitly requires otherwise. Check ordering, shapes, dtype, device, input mutation, and failure behavior as well as return values. Public API removals and renames require a deprecation and migration path.
- Prefer a functional approach for stateless computations and transformations: use plain functions with explicit inputs and outputs, keeping side effects at clear boundaries. Use classes when they own meaningful state or resources, enforce invariants over a lifecycle, or implement an interface required by the architecture. Avoid classes that only group static methods, wrap a single operation, or forward calls to another object. Preserve established public contracts when simplifying existing designs.
- Place shared operations in the existing module that owns their contract before adding a new file. Reuse existing mechanisms before introducing helpers, configuration options, or abstractions. Extract shared logic when it has the same contract; keep helpers private unless callers need a public API. Prefer direct control flow and early returns when they remove unnecessary nesting.
- Use predicates or optional lookup results for expected incompatibility, such as filtering available camera channels. Do not raise and catch exceptions for routine selection or capability checks.
- Inline simple expressions and operations when a helper would only add indirection. Do not extract a one-line helper merely to rename an obvious operation. Introduce a helper when it removes meaningful duplication or gives a coherent, non-trivial operation a useful name; its benefit should outweigh the need to jump to another definition to understand the caller. When retiring a workflow, remove its unused helper chains and tests that only preserve those helpers.
- Prefer direct attribute access and assignment (
obj.valueandobj.value = value). Usegetattrandsetattronly when dynamic attribute access is required, such as when the attribute name is determined at runtime. Do not use them for known attributes or use default values to hide a missing required attribute; express optional fields explicitly in the interface. - Give each piece of state and validation one owner. Consumers should use the owner's contract instead of repairing results or maintaining duplicate state. Cache derived values only when their lifetime and invalidation are clear; do not expose mutable cached results for callers to modify accidentally. Resolve selections once at initialization; backends should consume the final selection without a second filtering pass or cache. Before adding a parameter record and preparation helper for one consumer, check which values already exist in its configuration or array metadata. Keep the remaining setup with that owner and cache only the buffers or calculations that need reuse.
- Pass scene dependencies from the composition root into consumers. Do not retrieve the simulation
singleton to resolve a dependency the caller already owns. Resolve references at initialization,
then retain the resolved objects instead of copying paths between configuration fields. Give consumers
resolved resources rather than a broader construction plan used only to discover those resources;
visualizers receive bound camera choices, while cloning and renderer scene preparation retain
ClonePlan. - Put common configuration in the shared owner and document backend capabilities explicitly. Name collections in the plural. Remove empty hooks and expired compatibility aliases during their announced removal release instead of maintaining unused extension points.
- Keep backend selection at shared dispatch boundaries. Use established types, configuration, and capability contracts instead of inferring behavior from class-name strings.
- Keep physics and rendering responsibilities separate and resolve construction requirements before finalization. See :doc:`/source/developer-tools/scene_data_providers` for geometry ownership and :doc:`/source/developer-tools/add_physics_backend` for backend integration.
- Prefer existing project dependencies and the standard library. Do not add dependencies or compatibility layers for hypothetical future uses.
Review allocations, copies, synchronization, and recomputation in code that runs per step, reset, or environment. Costs that are small for one environment can dominate a large batch.
- Preserve slices through APIs that support them. Materialize indices only at a consumer that requires them, reusing cached device indices when available. Preserve the selector's ordering and device contract.
- Allocate arrays directly with the required value, dtype, and device. Prefer
torch.fullorwp.fullover filling through Python lists, arithmetic on temporary arrays, or a round trip through another library. - Keep operations on Warp-owned arrays in Warp. Use
ProxyArraywhen consumers need multiple array interfaces; do not convert to Torch and back merely to mutate a Warp buffer. - Keep per-step control flow direct, with one call to each lifecycle operation. Perform optional work only for the active consumer and preserve the configured update cadence.
- Remove redundant copies and
contiguous()calls only after checking layout and ownership requirements. Do not mutate caller-owned inputs unless the API explicitly promises an in-place operation. - Batch operations when supported. Avoid Python loops over environments and unnecessary host/device transfers or scalar reads that synchronize the device in hot paths.
- Allocate expensive optional buffers on first use when their lifecycle permits it. Account for graph capture: any required allocation must happen before capture when the allocator requires it.
- Use
snake_casefor functions, methods, and CLI arguments. Keep related public symbols discoverable through consistent prefixes and use existing API vocabulary. - Use concrete types where practical, built-in collection annotations such as
list[str], andX | Nonefor optional values. Keep argument and return types in signatures, without repeating them in docstrings. - Document public APIs with Google-style docstrings. State physical units inline, for example
Particle positions [m], shape [N, 3]. Use[m or rad, depending on joint type]for mixed joint quantities. Document coordinate frames and array shapes where relevant; indices, counts, and flags do not need physical units. - Keep comments brief and selective. Explain intent, non-obvious constraints, or edge cases that the code alone cannot make clear. Avoid narrating implementation steps or repeating what an expression does. Readability comments, section markers, and headings that help organize a file are welcome.
- Describe the current contract in code comments. Put migration instructions and descriptions of old behavior in public migration documentation or changelog entries, rather than leaving a history of refactoring in the implementation. Retain historical context only when it explains a constraint that still affects correctness.
- Update public documentation with API changes and verify technical claims against the implementation.
We follow a specific structure for the codebase. This helps in maintaining the codebase and makes it easier to understand.
Keep short expressions on one line within the configured limit and break longer expressions at
meaningful boundaries. Keep loop headers focused on iteration; unpack bulky nested records in the body.
Prefer descriptive names to new acronyms. Reuse matching sequences or mappings with */** instead
of unpacking and rebuilding them; do not introduce packing containers or reflective assignment just
to shorten code.
In a Python file, we follow the following structure:
# Imports: These are sorted by the pre-commit hooks.
# Constants
# Functions (public)
# Classes (public)
# _Functions (private)
# _Classes (private)Imports are sorted by Ruff through uv run isaaclab -f. The groups are __future__, standard library,
third-party packages, Omniverse runtime packages, Isaac Lab packages, and local relative imports; the
exact package groups are configured in pyproject.toml. Let the formatter apply this order.
Use relative imports within the same package when the target is at most three leading dots away
(for example, from ...utils import math as math_utils). Use absolute imports for deeper targets,
other packages, and modules that run as scripts with an if __name__ == "__main__": block.
Prefer module-level imports. A local import is appropriate when it defers an optional backend or simulator dependency until the selected runtime path needs it. Keep configuration imports usable before simulator startup and avoid repeating runtime initialization already owned by the package. To deal with circular imports, use the :obj:`typing.TYPE_CHECKING` variable. Please refer to the Circular Imports section for more details.
Public export modules in __init__.py are an exception to the above: they use
:func:`~isaaclab.utils.module.lazy_export` instead of traditional imports.
See the Lazy Loading & Module Exports section for details.
Pass ProxyArray objects directly to Warp kernels. Keep one proxy per owned array, without
parallel _ta, _warp, or _torch attributes; timestamped array caches can own the proxy in
data. Use explicit native access only where the receiving API requires it.
When a kernel operation is known before launch, specialize it with dedicated kernels or static Warp
branches instead of a runtime mode switch. Keep shared indexing in one implementation and benchmark
the generated kernels against the original path.
Python does not have a concept of private and public classes and functions. However, we follow the convention of prefixing the private functions and classes with an underscore. The public functions and classes are the ones that are intended to be used by the users. The private functions and classes are the ones that are intended to be used internally in that file. Irrespective of the public or private nature of the functions and classes, we follow the Style Guide for the code and make sure that the code and documentation are consistent.
When a class is warranted, order its members as follows:
# Constants
# Class variables (public or private): Must have the type hint ClassVar[type]
# Dunder methods, when needed: __init__, __del__
# Representation: __repr__, __str__
# Properties: @property
# Instance methods (public)
# Class methods (public)
# Static methods (public)
# _Instance methods (private)
# _Class methods (private)
# _Static methods (private)The rule of thumb is that the functions within the classes are ordered in the way a user would expect to use them. For instance, if the class contains the method :meth:`initialize`, :meth:`reset`, :meth:`update`, and :meth:`close`, then they should be listed in the order of their usage. The same applies for private functions in the class. Their order is based on the order of call inside the class.
Include only the members a class needs; this ordering is not a checklist of methods to implement.
For classes that own resources, expose explicit cleanup through close() or a context manager.
.. dropdown:: Minimal function example
:icon: code
.. literalinclude:: snippets/code_skeleton.py
:language: python
Circular imports happen when two modules import each other, which is a common issue in Python. You can prevent circular imports by adhering to the best practices outlined in this StackOverflow post.
In general, it is essential to avoid circular imports as they can lead to unpredictable behavior.
However, in our codebase, we encounter circular imports at a sub-package level. This situation arises
due to our specific code structure. We organize classes or functions and their corresponding configuration
objects into separate files. This separation enhances code readability and maintainability. Nevertheless,
it can result in circular imports because, in many configuration objects, we specify classes or functions
as default values using the attributes class_type and func respectively.
To address this, we use two complementary techniques:
- Resolvable strings — Store
class_typeandfuncas{DIR}-based strings (e.g."{DIR}.sensor:Sensor") so the implementation module is never imported at config construction time. The string is resolved to the actual class (via :class:`~isaaclab.utils.string.ResolvableString`) on invocation or attribute access that needs the implementation. Initialize any required runtime before triggering resolution. - TYPE_CHECKING guards — Import the implementation class under typing.TYPE_CHECKING so that IDEs and type checkers can provide autocomplete on the type annotation without triggering a runtime import.
See the Resolvable Strings and Lazy Loading & Module Exports sections for full examples of both patterns.
Use :func:`~isaaclab.utils.instantiate` to construct the implementation selected by a config:
from isaaclab.utils import clone, instantiate, replace, to_dict, update_from_dict, validate
robot_cfg = replace(ROBOT_CFG, prim_path="{ENV_REGEX_NS}/Robot")
other_cfg = clone(robot_cfg)
update_from_dict(other_cfg, {"init_state": {"pos": (1.0, 0.0, 0.0)}})
validate(other_cfg)
settings = to_dict(other_cfg)
robot = instantiate(robot_cfg)
action = instantiate(action_cfg, env)instantiate passes the config as the first constructor argument, followed by any additional
arguments. It does not copy configs, construct nested configs, or cache instances. Resource
sharing remains the responsibility of SimulationContext.get_or_create_backend.
clone and replace return new configurations, preserving fields explicitly marked as borrowed.
update_from_dict updates an existing configuration in place. validate checks required
fields and runs nested validate_config hooks. Prefer these functions in new code; the existing
cfg.copy(), cfg.replace(...), cfg.validate(), cfg.to_dict(), cfg.from_dict(...), and
cfg.class_type(cfg, ...) calls remain supported without deprecation.
The longer function names class_to_dict and update_class_from_dict also remain supported.
Use lazy loading for public package exports so that importing a top-level package
(e.g. import isaaclab.sensors) does not eagerly pull in heavyweight dependencies like
pxr, omni, or scipy. This is critical because config classes must be constructable
before SimulationApp is launched.
We follow SPEC 1 — Lazy Loading of Submodules and Functions and use the lazy_loader library (endorsed by NumPy, SciPy, scikit-image,
scikit-learn, NetworkX) with .pyi type-stub files. The stub is the single source of
truth for both IDE autocomplete and runtime lazy loading.
Standard pattern for public export modules:
# mypackage/__init__.py
from isaaclab.utils.module import lazy_export
lazy_export()With a corresponding type stub adjacent to it:
# mypackage/__init__.pyi
__all__ = ["MyClass", "MyOtherClass", "my_function"]
from .my_module import MyClass, MyOtherClass
from .my_other_module import my_functionKey rules for .pyi stubs:
- The
__all__list at the top marks names as public re-exports (per PEP 484). - Group imports from the same submodule on one line. Use parenthesized multi-line imports if the line exceeds 100 characters.
- Use relative imports (
from .something import ...) for local submodule symbols. Absolute wildcard imports (from pkg import *) are only used for cross-package fallbacks (see below). - Include the standard Isaac Lab license header.
Cross-package fallback — for modules that re-export names from another package
(e.g. task MDP modules that delegate to isaaclab.envs.mdp), add a wildcard
import for the external package in the .pyi stub:
# isaaclab_tasks/.../mdp/__init__.pyi
__all__ = ["MyReward", "MyObservation"]
from .rewards import MyReward
from .observations import MyObservation
from isaaclab.envs.mdp import *The __init__.py stays the same as the standard pattern — just lazy_export()
with no arguments:
# isaaclab_tasks/.../mdp/__init__.py
from isaaclab.utils.module import lazy_export
lazy_export()At runtime, lazy_export parses the .pyi stub and uses the absolute wildcard
import (from isaaclab.envs.mdp import *) as a fallback: any name not found in
the local submodules is looked up in the specified package. This also gives type
checkers and IDEs full visibility into the re-exported symbols.
Relative wildcard re-exports — the stub can also use from .submodule import *
to eagerly export all public names from a local submodule. This is resolved at
import time (not lazily). A large or frequently changing API alone does not justify eager imports.
Note
Relative wildcard re-exports bypass lazy loading and eagerly import every public name from the submodule at package init time. In general, we advise against using them unless absolutely necessary. Prefer listing explicit named imports in the stub so that the public API surface is clear, reviewable, and remains lazily loaded.
# isaaclab_tasks/.../mdp/__init__.pyi
from .rewards import *
from .observations import *
from isaaclab.envs.mdp import *Ensuring .pyi stubs are distributed
Declare stub files in the package's pyproject.toml so they are included in distributions:
[tool.setuptools.package-data]
"*" = ["*.pyi"]Keep this configuration in packages that provide lazy export stubs. The pre-commit insert-license hook
is configured to add license headers to .pyi files automatically (\.(pyi?|ya?ml)$).
When a config field needs to reference a class or callable that depends on the simulator
runtime, store it as a :class:`~isaaclab.utils.string.ResolvableString` rather than a
direct reference. This avoids eagerly importing heavyweight modules (omni, pxr,
etc.) at config construction time. Invocation or attribute access that needs the implementation triggers
resolution; the caller must initialize any required runtime first. Resolution does not itself wait for
SimulationApp, and workflows without Kit need not launch it.
You can use either the {DIR} shorthand or a fully-qualified module path:
# Good — {DIR} shorthand (resolved to the current package at runtime)
class_type: type[Sensor] | str = "{DIR}.sensor:Sensor"
# Good — fully-qualified path (useful for cross-package references)
class_type: type[Sensor] | str = "isaaclab.sensors.my_sensor.sensor:Sensor"
# Bad — eagerly imports the implementation module
from .sensor import Sensor
class_type: type = SensorThe config machinery expands {DIR} to the package of the field's defining class
(e.g. isaaclab.sensors.my_sensor), without importing the referenced implementation. Prefer {DIR}
for references within the same package since it stays correct across renames and moves.
For the type annotation (type[Sensor]), import the class under a TYPE_CHECKING guard
so that the IDE can still provide autocomplete without triggering a runtime import:
from __future__ import annotations
import typing
if typing.TYPE_CHECKING:
from .sensor import SensorFor components with configuration classes, keep the configuration and runtime implementation in separate files so importing configuration does not load heavy runtime dependencies. This pattern does not require introducing a class or configuration object for a stateless function:
my_sensor/
├── __init__.py # lazy_export()
├── __init__.pyi # re-exports: SensorCfg, Sensor
├── sensor_cfg.py # pure data — no runtime deps
└── sensor.py # implementation — may import omni, pxr, etc.
__init__.py — uses lazy_export() to lazily load names from the stub:
# my_sensor/__init__.py
from isaaclab.utils.module import lazy_export
lazy_export()__init__.pyi — declares the public API for both IDE autocomplete and lazy loading:
# my_sensor/__init__.pyi
__all__ = ["SensorCfg", "Sensor"]
from .sensor_cfg import SensorCfg
from .sensor import Sensorsensor_cfg.py — pure data; references the implementation class by resolvable string
to avoid importing it:
# my_sensor/sensor_cfg.py
from __future__ import annotations
import typing
from isaaclab.utils import configclass
if typing.TYPE_CHECKING:
from .sensor import Sensor
@configclass
class SensorCfg:
class_type: type[Sensor] | str = "{DIR}.sensor:Sensor"sensor.py — the implementation; imports runtime dependencies only when needed:
# my_sensor/sensor.py
from pxr import Usd
from .sensor_cfg import SensorCfg
class Sensor:
def __init__(self, cfg: SensorCfg, stage: Usd.Stage) -> None:
self.cfg = cfg
self.stage = stageUse specific type hints in function signatures and class attributes. Describe meaning, units, shapes, and constraints in docstrings without repeating the annotated types:
def add(a: int, b: int) -> int:
"""Add two integers.
Args:
a: The first operand.
b: The second operand.
Returns:
The sum of the operands.
"""
return a + b- Prefer built-in collection types such as
list[str]anddict[str, int]. - Use
X | Nonefor optional values. - Use
TYPE_CHECKINGand deferred annotations when types require runtime-only imports. - Annotate functions that return no value with
-> None. Omit an unnecessaryReturns:section from their docstrings.
Choose the mechanism by who has to act on the message, following the Python logging HOWTO:
| Situation | Mechanism |
|---|---|
| The caller should change their code or config: a deprecated API or parameter, an ignored or conflicting setting, or misuse. | warnings.warn(message, <Category>, stacklevel=...) |
| A runtime event that the caller cannot avoid by changing their code: a fallback was taken, an optional dependency or feature is unavailable, or performance is degraded. | logger.warning(message) with a module-level logger = logging.getLogger(__name__) |
| User-facing notices in scripts and command-line tools. | logger.warning(message), as above |
| Progress and status from library code and entry points: the parsed task configuration, the log directory, environment and manager summaries. | logger.info(message) |
| A failure reported before exiting a command-line tool. | logger.error(message) |
- Always pass an explicit category to
warnings.warn:DeprecationWarningfor deprecated Python APIs that callers use from their own code.FutureWarningfor deprecated configuration values, presets, or command-line options that end users reach through Isaac Lab entry points. Python shows these by default even when they are raised from library code.UserWarningfor misuse or for settings that are ignored.
- Set
stacklevelso that the warning points at the caller's line, not at Isaac Lab internals. - Do not use
printfor warnings or status messages, and do not add[WARNING],[WARN],[INFO], or[ERROR]prefixes. Printed messages ignore--verbose/--infoand log handlers, and tests cannot capture them reliably. The logging record already carries the level.printremains the right tool for a program's actual output, such as command results, a--dry_runcommand line, or the tables a tutorial walks through. - Isaac Lab entry points and :func:`~isaaclab.app.launch_simulation` call
isaaclab.app.logging_utils.configure_console_logging, which prints INFO records fromisaaclab*loggers on stdout as[INFO]: <message>and warnings on stderr. Call it first in a new command-line entry point so that messages logged before the simulation runtime starts are shown. - In tests, assert
warnings.warnwithpytest.warnsandlogger.warningwithcaplog.
import logging
import warnings
logger = logging.getLogger(__name__)
def set_gains(stiffness: float, damping: float | None = None, kd: float | None = None) -> None:
if kd is not None:
warnings.warn("'kd' is deprecated. Use 'damping' instead.", DeprecationWarning, stacklevel=2)
damping = kd
...
def capture_graph() -> None:
try:
...
except RuntimeError as exc:
logger.warning(f"CUDA graph capture failed; falling back to eager launches. Reason: {exc}")Write documentation that lets a caller use the API without reading its implementation.
- State the behavior first, then explain constraints, defaults, and relevant failure modes.
- Document units, coordinate frames, shapes, and ownership or mutation of inputs and returned data.
- Explain non-obvious design constraints in brief comments near the relevant code.
- Use a small example when it clarifies usage. Avoid repeating the signature or narrating each operation.
- Use plain language and active voice; remove repetition and vague qualifiers.
- Update documentation when behavior changes and check examples against the current implementation.
We use pytest for unit testing. Keep coverage lean and fast by giving each contract one primary test owner at the strongest observable boundary. Start with existing coverage at that boundary and extend it when it can clearly cover the changed behavior.
Apply the authoring gate and retention criteria in the test-audit skill when adding, changing, reviewing, or pruning tests. It maintains the detailed criteria for deciding whether a test earns its cost.
- Add a test only for a distinct behavior, regression, boundary, or failure mode that existing coverage does not already exercise. Formatting and mechanical cleanup do not automatically require new tests. Before adding it, identify the protected contract, a credible regression, and why existing coverage would miss that regression. Do not add production exports, flags, wrappers, or injection hooks solely to support a test; exercise the real boundary instead.
- Test observable behavior and public contracts. Avoid assertions tied to private implementation details or expected values computed by repeating the production algorithm. Avoid assertion-free smoke tests, self-comparisons, and mocks or fixtures that supply the very behavior the test claims to verify.
- Verify that regression tests fail without the fix for the intended reason and pass with it. Cover the bug at its owning boundary; another layer or backend needs a distinct risk to justify replaying it.
- Keep parameter matrices and simulation fixtures focused on distinct execution paths. Consolidate redundant coverage instead of adding overlapping cases or rebuilding the same scene unnecessarily. Avoid Cartesian products of devices, shapes, and environment counts when the axes do not exercise distinct paths. Reuse the fixture that already establishes the contract.
- Before removing or merging coverage, identify the proof that remains and demonstrate that it fails when the contract is broken, using a focused mutation of the production owner where appropriate. Preserve distinct edge cases and independent API, physical, backend-parity, and packaging contracts. A slow test or similar-looking assertions alone are not evidence of duplication.
- Run the narrowest relevant test first. If an optional dependency is missing, identify its project extra
and retry with
uv run --extra <extra> python -m pytest .... For changes intended to reduce test time, measure before and after on the same machine and separate kernel compilation from execution time.
Use the same commands on Linux and Windows:
# Run a particular test
uv run python -m pytest source/isaaclab/test/utils/test_circular_buffer.py::test_reset
# Run all tests in a particular file
uv run python -m pytest source/isaaclab/test/utils/test_circular_buffer.py
# Run source-package tests through the repository test runner
uv run python tools/run_all_tests.py
# Run tooling tests under tools/
uv run isaaclab --testAll of these commands exit with a nonzero code when tests fail, so a test failure fails the invoking shell or CI step as well.
We use the following tools for maintaining code quality:
- pre-commit: Runs a list of formatters and linters over the codebase.
- ruff: An extremely fast Python linter and formatter.
Run the repository formatting and lint checks from the uv-managed environment on Linux or Windows:
uv run isaaclab --formatDuring editing, pass repository-relative file paths to restrict file-based hooks:
uv run isaaclab --format source/isaaclab/isaaclab/cli/commands/format.pyRepository-wide hooks such as the changelog gate still run. The command runs pre-commit once and returns its failure status, including when hooks modify files. Inspect those edits and rerun after resolving failures; it does not automatically replay all hooks. Run the full command on the final changes before committing.