Python coding conventions for Claude Forge.
- Public before Private: Put public methods before private ones
- Class Structure: Constants -> class variables ->
__init__-> public methods -> private methods - Module Structure: Imports -> constants -> classes -> functions -> main block
- snake_case: Variables, functions, methods (
get_context,create_session) - CamelCase: Classes (
SessionManager,ModelCatalog) - UPPER_CASE: Constants (
MAX_TOKENS,DEFAULT_PORT) - _leading_underscore: Private methods/variables (
_build_manifest,_cache)
- Type Hints Required: Public functions must annotate params + return types
- Type Narrowing: Accept base types; narrow with
isinstance()at runtime# Good (LSP) async def process(self, config: BaseConfig) -> Result: if not isinstance(config, SessionConfig): raise TypeError(f"Expected SessionConfig, got {type(config)}") session_config: SessionConfig = config # Now safe # Bad (violates LSP) async def process(self, config: SessionConfig) -> Result:
- No TYPE_CHECKING workarounds: Fix circular imports instead of
if TYPE_CHECKING:blocks
- No Event Loop Nesting: Don't create loops to sync async (except app top-level)
- Propagate async: If a function uses async, make it async and
await - Sync Wrappers: API boundaries only, when necessary
These are consumed by external users and automation. Breaking changes require:
- Deprecation period: at least one minor release with a deprecation warning before removal.
- CLI aliases: old command/option name kept as a hidden alias during the deprecation period when possible.
- File format migration: schema version bump +
forge migratecommand or clear upgrade instructions. - Changelog entry: every breaking change documented with migration steps.
Use visible CLI output (click.echo("Deprecated: ...", err=True) or Rich stderr) so users see command changes before
they break. For documented Python APIs, use FutureWarning or a project-specific warning with stacklevel=2;
DeprecationWarning is usually hidden outside __main__.
Internal code not exposed to users follows a clean-break policy:
- Update callers directly: don't add adapters or compatibility wrappers.
- No fallback logic: when replacing a component, remove the old one.
- Change interfaces atomically: update all callers in the same commit.
- Delete obsolete tests: when removing functionality, delete its tests (don't skip).
- Removed code → delete test
- Moved behavior → update test
- Don't accumulate skips
Three boundary types determine error-handling policy:
- Reject invalid input. Raise specific exceptions.
- No warn+ignore. No silent defaults. No fallback logic.
- Proxy/resource lookups by config reference are internal — if a configured name can't resolve, the lookup raises. Only the outermost caller (hook adapter, CLI) decides whether that failure is blocking or a warning.
Forge-owned durable state = registries, indexes, manifests, session files — anything Forge writes to disk for later reads.
- Version fields are mandatory on all persisted schemas.
- Read with strict deserialization. Unknown fields are corruption, not forward compat.
- Unsupported schema versions produce a clear error ("written by newer Forge — upgrade").
- The first OSS release may reset public state formats to version
1, but version1must mean exactly the public v1 shape. If pre-OSS files at the same path also used1, readers must distinguish by structure and either:- Reject with a clear reset/migration message for durable state, or
- Discard and recreate runtime-only state (for example
active.json).
- Post-OSS schema changes require a version bump plus either:
- An explicit migration command (
forge migrate), or - A documented breaking release (users delete and recreate).
- An explicit migration command (
- Never use
strict=Falseor silent entry skipping on durable state. - Pre-release (
0.x) versions may break formats without a deprecation period, but still need strict shape validation and clear changelog/reset instructions.
External data: user-edited config, LLM responses, subprocess output, Claude Code hook payloads.
- Critical path: Fail with a clear error message.
- Best-effort (non-critical path): Warn and degrade to a safe default. Example: unknown key in user config YAML → warn, ignore. Example: supervisor parse failure → "aligned" (design.md §4.1.2 mandates fail-open for policy evaluations).
Best-effort patterns MUST:
- Log at warning or debug level (never silent)
- Document the degradation intent in a comment
- Degrade to a safe default (not an arbitrary one)
- Specific Exceptions: Prefer
TypeError/ValueError(notException) - Meaningful Messages: Include context in error messages
- Context Managers: Use
withfor cleanup
AI-assisted coding often produces oververbose comments. Every comment must earn its place.
Code shows how. Comments explain why. Restating code is noise.
# Bad — duplicates the code
user_count = len(users) # Count users
# Good — explains WHY, not what
active_users = [u for u in users if not u.is_service_account] # Service accounts skew metrics
# Good — documents a non-obvious constraint
def get_session_by_name(name: str) -> Session:
"""Look up session; fall back to partial match.
Needed because Claude Code truncates session names to 40 chars
in .claude/settings.json.
"""- Non-obvious "why": Rules, constraints, performance
- Workarounds and bug fixes: Link issue; explain workaround
- Unidiomatic code: If you deviate, say why
- External references: Link specs/RFCs/docs
- Invariants and assumptions: Non-obvious constraints (caller holds lock, sorted, max 100 entries)
- Lint/type suppressions: Any
# noqaor# type: ignoremust include a reason.# type: ignore[arg-type] # Pydantic coerces str→Path at runtime - TODO markers:
# TODO(#issue): ...with context (not bare# TODO)
- Obvious operations:
i += 1,return result,self.name = name - What the function signature already says: Type hints + clear name > restating docstring
- Commenting out dead code: Delete it (git remembers). Includes debugging leftovers (
# print(...),# import pdb). - Apologetic comments:
# This is a hack/# Sorry, this is ugly— fix the code - Section separators:
# ===== HELPERS =====— use module structure, not ASCII art - Closing bracket labels:
# end if/# end for— if you need these, the block is too long; extract a function
- Public API: Required. One-line summary; add detail only if name + signature aren't enough. Include Args/Returns/Raises when semantics aren't obvious from types (side effects, units, ranges, errors).
- Private methods: Optional. Skip if the name is self-explanatory. Add if the logic is non-trivial.
- Modules: Optional. Use if the module's purpose isn't obvious from name + contents.
- Format: Use imperative mood ("Return the session" not "Returns the session").
# Good — tells you something the signature doesn't
def resolve_model(tier: str, family: str) -> str:
"""Map user-facing tier (haiku/sonnet/opus) to backend model ID.
Falls back to family default if tier is missing. Raises ValueError
if family is unknown (no silent degrade).
"""
# Bad — Args/Returns that restate type hints add nothing
def resolve_model(tier: str, family: str) -> str:
"""Resolve a model from a tier and family.
Args:
tier: The tier to resolve.
family: The family to resolve.
"""When AI generates code, watch for these comment anti-patterns:
- Play-by-play narration: Comment on every line explaining Python syntax
- Redundant section headers:
# Initialize variablesabove three obvious assignments - Filler preambles:
# First, we need to.../# Now let's...— code speaks for itself - Docstrings that restate the function name:
def create_session→"""Create a session."""— delete this, it adds nothing - Over-qualifying with "Note:":
# Note: returns None if not found— just say# Returns None if not found - Invented rationales:
# for performance/# for safetywithout evidence — delete unless verified
Strip these during review. Don't add comments to untouched code.