Loom Coding, Documentation & Style Guide¶
This guide defines the coding, commenting, and documentation standards for all Loom contributors. Code that does not conform will be flagged during review or by the automated linter.
Read CONTRIBUTING.md first for architectural invariants and
the CLA. This guide covers the how of writing code; CONTRIBUTING.md covers
the what is acceptable.
Table of Contents¶
- Python Version and Language Features
- Code Formatting
- Naming Conventions
- Import Style
- Type Annotations
- Module Docstrings
- Class Docstrings
- Function and Method Docstrings
- Inline Comments
- Error Handling
- Logging
- Testing
- YAML Configuration
- Git Workflow
- Reference Examples
Python Version and Language Features¶
- Python 3.11+ is required. Use modern syntax freely:
X | Yunion syntax (notUnion[X, Y])dict[str, Any]lowercase generics (notDict[str, Any])from __future__ import annotationsat the top of every module (PEP 563 deferred evaluation — keeps runtime import cost low and avoids forward-ref issues)- Pydantic v2 for all data models.
- asyncio for all I/O-bound code. Actors are async; blocking calls must be
offloaded to a thread pool (see
SyncProcessingBackend).
Code Formatting¶
All formatting is enforced by ruff (configured in pyproject.toml).
| Rule | Setting |
|---|---|
| Line length | 100 characters |
| Indentation | 4 spaces (no tabs) |
| Quotes | Double quotes (") preferred |
| Trailing commas | Required in multi-line constructs |
| Blank lines | 2 between top-level definitions, 1 within classes |
Run the formatter before committing:
Naming Conventions¶
| Element | Convention | Example |
|---|---|---|
| Modules | snake_case |
nats_adapter.py |
| Classes | PascalCase |
PipelineOrchestrator |
| Functions / methods | snake_case |
resolve_tier() |
| Constants | UPPER_SNAKE_CASE |
DEAD_LETTER_SUBJECT |
| Private members | Leading underscore | _running, _refill() |
| Type variables | PascalCase + T suffix |
MessageT |
| Pydantic fields | snake_case |
worker_type, goal_id |
| NATS subjects | dot.separated.lowercase |
loom.tasks.incoming |
| CLI commands | kebab-case (Click default) |
loom workshop |
| Config keys (YAML) | snake_case |
max_concurrent_goals |
Abbreviations: Avoid unless universally understood (url, id, db).
Spell out domain terms (orchestrator not orch, message not msg — except
in structlog event names where brevity is expected).
Import Style¶
Imports are organized into four groups separated by blank lines, sorted alphabetically within each group:
# 1. __future__ imports (always first)
from __future__ import annotations
# 2. Standard library
import asyncio
import json
from typing import Any
# 3. Third-party
import structlog
import yaml
from pydantic import BaseModel, Field
# 4. Local (loom package)
from loom.core.actor import BaseActor
from loom.core.messages import TaskMessage, TaskResult
Rules:
- Use
from X import Yfor specific names; useimport Xfor namespaces you reference multiple times (e.g.,import jsonthenjson.loads()). - Never use wildcard imports (
from X import *). - Conditional imports (for optional dependencies) go at point of use, not at module top:
# Good — only imported when needed, avoids hard dep
async def process(self, ...):
from loom.worker.knowledge import load_knowledge_silos
...
- Ruff enforces import sorting automatically via
isortrules.
Type Annotations¶
- Annotate all public function signatures (parameters and return types).
- Private helper functions: annotations recommended but not strictly required.
- Use
Anysparingly — prefer specific types.dict[str, Any]is acceptable for JSON-like data flowing across actor boundaries. - Use
| Noneinstead ofOptional[X]. - For callback types, use
collections.abc.Callable(nottyping.Callable).
# Good
async def call_worker(
self,
worker_type: str,
payload: dict[str, Any],
tier: str = "standard",
timeout: float = 60.0,
) -> dict[str, Any]:
...
# Bad — missing return type, uses old-style Optional
def call_worker(self, worker_type, payload, tier="standard", timeout=60.0):
...
Module Docstrings¶
Every .py file must have a module-level docstring immediately after the
""" opening. This is the single most important piece of documentation — it
tells a reader what this file does and why it exists without reading any
code.
Required elements:
- One-line summary — what the module does.
- Context paragraph — where this fits in the architecture, what depends on it, and what it depends on.
- Design notes (if applicable) — why a particular approach was chosen, any invariants maintained, known limitations.
- See also (if applicable) — related modules for navigation.
Template:
"""
One-line summary of what this module does.
Longer description providing architectural context: how this module fits into
Loom, what calls it, what it calls. Explain the core abstraction or pattern.
Design note: why this approach was chosen over alternatives. For example,
why we use our own JSON Schema validator instead of the jsonschema library.
See also:
loom.core.messages — the message types this module processes
loom.bus.nats_adapter — the production bus implementation
"""
Good example (from core/actor.py):
"""
Base actor class — the foundation of Loom's actor model.
All Loom actors (workers, orchestrators, routers) inherit from BaseActor.
This class handles the message bus subscription lifecycle, message dispatch,
signal-based shutdown, and error isolation. Each actor is an independent
process with no shared memory.
Design invariant: actors communicate ONLY through bus messages (see messages.py).
Direct method calls between actors are forbidden.
The message bus is pluggable via the ``bus`` constructor parameter. The default
is NATSBus (created from ``nats_url`` when no bus is provided). For testing,
pass an InMemoryBus instead.
"""
Class Docstrings¶
Every public class must have a docstring explaining:
- What it is (one-line summary).
- How to use it — constructor parameters, key methods, expected lifecycle.
- Invariants — what guarantees it maintains (e.g., "stateless between tasks", "thread-safe", "not safe for concurrent use").
Use reStructuredText-style cross-references for related classes:
class PipelineOrchestrator(BaseActor):
"""
Pipeline orchestrator with automatic stage parallelism.
Processes an OrchestratorGoal by running it through a series of stages
organized into execution levels based on their dependencies. Stages
within the same level run concurrently; levels execute sequentially.
Stage outputs are accumulated in a context dict and can be referenced
by subsequent stages via input_mapping.
"""
Private/internal classes: A brief one-line docstring is sufficient.
Function and Method Docstrings¶
When to write a docstring¶
| Visibility | Rule |
|---|---|
| Public API (no underscore) | Always — full docstring |
Protected (_single_underscore) |
Required if non-trivial (>10 lines or complex logic) |
Private (__double_underscore) |
Optional — brief comment often suffices |
Dunder methods (__init__, __aiter__) |
Required if they accept non-obvious parameters |
Docstring format¶
Use Google-style docstrings (compatible with Sphinx napoleon extension):
async def call_worker(
self,
worker_type: str,
payload: dict[str, Any],
tier: str = "standard",
timeout: float = 60.0,
) -> dict[str, Any]:
"""Dispatch a task to a worker and wait for the result.
Publishes a TaskMessage to loom.tasks.incoming and subscribes to
the result subject. Blocks until a matching TaskResult arrives or
the timeout expires.
Args:
worker_type: Which worker config to dispatch to (e.g., "summarizer").
payload: Structured input conforming to the worker's input_schema.
tier: Model tier override. Defaults to "standard".
timeout: Maximum seconds to wait for a result.
Returns:
The worker's output dict (the ``output`` field of TaskResult).
Raises:
BridgeTimeoutError: If no result arrives within ``timeout`` seconds.
BridgeError: If the worker returns a FAILED status.
"""
Rules¶
- First line is a concise imperative summary ("Dispatch a task", not "Dispatches a task" or "This method dispatches a task").
- Blank line between summary and body.
- Args section: list every parameter (except
self/cls). Include types only if they add clarity beyond the annotation. - Returns section: describe the return value structure. For dicts, mention key fields.
- Raises section: list exceptions the caller should handle. Omit generic
exceptions that indicate bugs (e.g.,
TypeError). - Keep docstrings accurate — an outdated docstring is worse than none. If you change a function's behavior, update the docstring in the same commit.
Inline Comments¶
When to comment¶
- Why, not what. Don't restate the code. Explain the reasoning behind a non-obvious choice.
- Gotchas and edge cases — especially Python quirks (e.g.,
boolis a subclass ofint). - TODO markers — use
# TODO: Strategy X — ...for planned future work. Reference the strategy letter from CLAUDE.md so the item is traceable. - Performance notes — if code is written a certain way for performance, say so.
Style¶
# Good — explains WHY
# Reject bools masquerading as ints (bool is a subclass of int)
if isinstance(value, bool) or not isinstance(value, int):
# Bad — restates the code
# Check if the value is a bool or not an int
if isinstance(value, bool) or not isinstance(value, int):
# Good — marks a design decision
# Sequential processing — strict mailbox semantics
await self._process_one(data)
# Good — TODO with strategy reference
# TODO: Strategy A — streaming result collection
Section headers¶
For long methods or complex logic, use section comment headers:
# ------------------------------------------------------------------
# Dependency inference and execution level construction
# ------------------------------------------------------------------
Keep these consistent: 70-char dashes, no blank line before the first line after the header.
Error Handling¶
- Raise specific exceptions, not generic
ExceptionorRuntimeError. Define custom exception classes for each module's failure modes:
class PipelineStageError(Exception):
"""Raised when a pipeline stage fails or times out."""
def __init__(self, stage_name: str, message: str):
self.stage_name = stage_name
super().__init__(message)
- Don't silence exceptions without logging:
# Good
except Exception as e:
logger.error("actor.error", actor_id=self.actor_id, error=str(e))
# Bad
except Exception:
pass
- Actor isolation: individual message failures must not crash the actor loop. Catch at the message handler level and log.
- Validate at boundaries: check inputs at actor/API boundaries (contract validation, message parsing), trust internal code.
Logging¶
Use structlog everywhere. Never use print() for operational output.
Event naming convention¶
Examples: actor.connected, router.dead_letter, worker.tool_round,
pipeline.stage_completed.
Log levels¶
| Level | Use for |
|---|---|
debug |
Internal state details (message contents, intermediate values) |
info |
Normal operational events (connected, subscribed, task routed) |
warning |
Recoverable issues (unknown tool, condition parse failure, rate limit) |
error |
Failures that affect the current operation (task failed, backend error) |
Structured fields¶
Always pass context as keyword arguments, not interpolated strings:
# Good
logger.info("router.routing", task_id=task.task_id, tier=tier.value)
# Bad
logger.info(f"Routing task {task.task_id} to tier {tier.value}")
Testing¶
File naming¶
- Test file:
test_{module_name}.py(mirrorssrc/loom/{package}/{module}.py) - Test class:
Test{ClassName}(e.g.,TestPipelineOrchestrator) - Test function:
test_{behavior_description}(e.g.,test_stage_timeout_produces_failed_result)
Test organization¶
"""Tests for loom.orchestrator.pipeline — PipelineOrchestrator."""
import pytest
from loom.orchestrator.pipeline import PipelineOrchestrator, PipelineStageError
class TestBuildExecutionLevels:
"""Execution level construction from dependency graphs."""
def test_independent_stages_in_single_level(self):
...
def test_circular_dependency_raises(self):
...
class TestExecuteStage:
"""Single-stage execution with mocked bus."""
@pytest.fixture
def pipeline(self):
...
Rules¶
- No infrastructure required for unit tests. Use
InMemoryBusandInMemoryCheckpointStore. - Mark integration tests with
@pytest.mark.integration. - Every new feature must include unit tests. PRs without tests for new code will not be merged.
- Test the contract, not the implementation. If you test internal methods directly, your tests are coupled to implementation details.
- Use
pytest.fixturefor shared setup. Keep fixtures close to where they're used (same file orconftest.py). asyncio_mode = "auto"is configured — async test functions work without the@pytest.mark.asynciodecorator.
Coverage¶
- Minimum threshold: 70% (enforced in CI via
fail_under). - Target: 85%+ for core modules (
core/,worker/,orchestrator/,router/,bus/). - Use
# pragma: no coveronly for truly unreachable code (e.g.,if __name__ == "__main__"guards,TYPE_CHECKINGblocks).
YAML Configuration¶
Worker and pipeline configs live in configs/. Follow these conventions:
- Top-level keys in
snake_case. - Include a comment header explaining what the config does:
# summarizer.yaml — Summarize text into structured output.
# Tier: local (Ollama). See _template.yaml for all available keys.
name: summarizer
worker_type: summarizer
- Schema fields (
input_schema,output_schema) must be valid JSON Schema (type: object, with properties and required arrays). - Keep configs narrow — one responsibility per worker. Don't combine summarization and classification in a single config.
Git Workflow¶
- Branch naming:
feature/description,fix/description,docs/description. - Commit messages: imperative mood, concise summary line (<72 chars), with optional body explaining why:
Add dependency inference to PipelineOrchestrator
Stages now auto-infer dependencies from input_mapping paths instead of
requiring explicit depends_on lists. Uses Kahn's algorithm for
topological sort to detect cycles at config load time.
- One logical change per commit. Don't mix refactors with features.
- Run
uv run ruff check src/ && uv run pytest tests/ -v -m "not integration"before pushing.
Reference Examples¶
The following source files exemplify these standards and should be used as references when writing new code:
| Pattern | Reference file |
|---|---|
| Module docstring with architecture context | src/loom/orchestrator/pipeline.py |
| Class with lifecycle documentation | src/loom/core/actor.py |
| Function with Args/Returns/Raises | src/loom/worker/runner.py (execute_with_tools) |
| Custom exception hierarchy | src/loom/orchestrator/pipeline.py |
| ABC with usage examples in docstring | src/loom/bus/base.py |
| Pydantic models with field documentation | src/loom/core/messages.py |
| Section headers in long classes | src/loom/orchestrator/pipeline.py |
| Structured logging conventions | src/loom/router/router.py |
| Contract validation with design rationale | src/loom/core/contracts.py |
| Contrib backend with config docs | src/loom/contrib/rag/backends.py |