Dedalus Style Guide: Quick Reference
Hard Limits
- Functions: 70 lines max
- Files: 500 lines max (excluding inline tests)
- Nesting: 3 levels max
- Arguments: 5 max (excluding self/cls)
- PRs: 200 changed LOC max
I/O Convention
# I/O function: io: Io as first parameter
async def fetch_user(io: Io, user_id: str) -> User | None: ...
# Pure function: no io parameter
def compute_discount(user: User, cart: Cart) -> Decimal: ...
Types
- No
Any. Period. If you reach for Any, you have not modeled the domain.
- No
object as a type annotation. It is Any in disguise.
- For heterogeneous JSON, use
JSONValue, JSONObject, JSONPrimitive, or
JSONArray from core.types.json. These are proper recursive RFC 8259 types.
There is no excuse for dict[str, object] or dict[str, Any].
- No
**kwargs: Any without justification.
- Use typed structures (dataclass, TypedDict, Pydantic) instead of dicts.
- Pydantic at boundaries, plain types internally.
- Replace anonymous tuples with frozen dataclasses or named types.
Functions
- Early returns instead of nesting
- One thing per function: if you need "and", split it
- Named parameters at call sites:
fetch_user(io=io, user_id=uid)
- Return value in own variable for debuggability:
result = ...; return result
- No chaining construction + method:
Foo(x).bar() hides the instance.
Split into foo = Foo(x) then result = foo.bar().
- Split compound assertions:
assert a; assert b not assert a and b
Error Handling
- Specific exceptions with domain context:
ChargeError(org_id, reason)
- Never catch bare
Exception without # noqa: BLE001
- Never swallow with
pass
- Namespace pattern:
BillingError.ChargeError
- Result types for domain logic, try/except at boundaries
No Fallbacks
# Bad: silent, undebuggable
model = request.model or config.default_model or "gpt-4"
# Good: explicit precedence with early returns
def get_model(request, config):
if request.model:
return request.model
if config.default_model:
return config.default_model
raise ValueError("model is required")
Naming
- No abbreviations:
get_user_by_id not get_usr
- Units last:
timeout_ms, latency_ms_p99
- Terse locals for mechanical:
res, ret, cfg, ctx
- Descriptive for domain:
user, balance, org_id
- Loop variables: full singular form.
for field in fields, not for f in fields.
Applies in comprehensions too: {field.name: field for field in fields}.
Docstrings (Google style)
async def fetch_user(io: Io, user_id: str) -> User | None:
"""Fetch user from database by ID.
The returned User is a snapshot; mutations won't persist.
Args:
io: I/O capability handle.
user_id: The unique identifier for the user.
Returns:
The User object if found, None otherwise.
"""
- Imperative mood: "Fetch", not "Fetches"
- Args section if function takes parameters
- Returns section always (even for None)
- Blank line before closing
"""
- Full sentences with periods. No dashes or sentence fragments.
Comments
- Explain WHY, not WHAT
- Delete narration:
user = get_user(id) # Get the user → delete the comment
- Document trade-offs and policy choices
- Section headers: single-line
# --- Label --- (no multi-line box separators)
- Column-align inline field comments with
# fmt: off / # fmt: on
(see column-aligned-fields skill for detailed rules)
Module Organization
__init__.py: bare docstring only. No imports, no re-exports, no __all__.
- No
__all__ in implementation files either. Public API is communicated by
naming convention (public vs _-prefixed). We never use import *.
- One class per stage file. Class absorbs all logic (not a thin wrapper).
- Prefer public methods. Only
_-prefix true internals callers never need.
- Types (dataclasses, aliases) in
types.py. Logic in the main module.
- No backward-compat shims, convenience wrappers, or
_default_instance patterns.
- Use
functools.cache for expensive I/O loads, not manual ClassVar[dict]
caches. Don't import cross-package deps for simple stdlib needs.
Constants
- Use Enum or frozen dataclass, not raw dicts/lists
- Numeric literals > 999 need underscores:
100_000 not 100000
- Powers of 2 are exempt:
4096 is fine
Testing (Inline)
# --- Tests ---
from inline_tests import test # noqa: E402
@test
async def rejects_empty_id():
import pytest # noqa: PLC0415
with pytest.raises(ValueError):
await fetch_user("")
- Colocated with implementation
- Local imports with
# noqa: PLC0415
- One test = one scenario
- Unit tests (synthetic data) inline, integration tests (real files) in
tests/
- Tests verify behavioral contracts, not implementation details or Python builtins
- Run:
uv run pytest path/to/file.py --inline-tests -v
Before Committing
uv run ruff format <files>
uv run ruff check <files> --fix
- Each file <= 500 production lines
- Each function <= 70 lines
- No
Any without justification
- No
__all__ in any implementation file
1---2name: style-guide3description: Dedalus style guide quick reference. Key rules for writing code that lasts. Use when writing or reviewing Python code.4---56# Dedalus Style Guide: Quick Reference78## Hard Limits910- Functions: **70 lines max**11- Files: **500 lines max** (excluding inline tests)12- Nesting: **3 levels max**13- Arguments: **5 max** (excluding self/cls)14- PRs: **200 changed LOC max**1516## I/O Convention1718```python19# I/O function: io: Io as first parameter20async def fetch_user(io: Io, user_id: str) -> User | None: ...2122# Pure function: no io parameter23def compute_discount(user: User, cart: Cart) -> Decimal: ...24```2526## Types2728- No `Any`. Period. If you reach for `Any`, you have not modeled the domain.29- No `object` as a type annotation. It is `Any` in disguise.30- For heterogeneous JSON, use `JSONValue`, `JSONObject`, `JSONPrimitive`, or31 `JSONArray` from `core.types.json`. These are proper recursive RFC 8259 types.32 There is no excuse for `dict[str, object]` or `dict[str, Any]`.33- No `**kwargs: Any` without justification.34- Use typed structures (dataclass, TypedDict, Pydantic) instead of dicts.35- Pydantic at boundaries, plain types internally.36- Replace anonymous tuples with frozen dataclasses or named types.3738## Functions3940- **Early returns** instead of nesting41- **One thing per function**: if you need "and", split it42- **Named parameters** at call sites: `fetch_user(io=io, user_id=uid)`43- **Return value in own variable** for debuggability: `result = ...; return result`44- **No chaining construction + method**: `Foo(x).bar()` hides the instance.45 Split into `foo = Foo(x)` then `result = foo.bar()`.46- Split compound assertions: `assert a; assert b` not `assert a and b`4748## Error Handling4950- **Specific exceptions** with domain context: `ChargeError(org_id, reason)`51- **Never** catch bare `Exception` without `# noqa: BLE001`52- **Never** swallow with `pass`53- **Namespace pattern**: `BillingError.ChargeError`54- **Result types** for domain logic, try/except at boundaries5556## No Fallbacks5758```python59# Bad: silent, undebuggable60model = request.model or config.default_model or "gpt-4"6162# Good: explicit precedence with early returns63def get_model(request, config):64 if request.model:65 return request.model66 if config.default_model:67 return config.default_model68 raise ValueError("model is required")69```7071## Naming7273- No abbreviations: `get_user_by_id` not `get_usr`74- Units last: `timeout_ms`, `latency_ms_p99`75- Terse locals for mechanical: `res`, `ret`, `cfg`, `ctx`76- Descriptive for domain: `user`, `balance`, `org_id`77- Loop variables: full singular form. `for field in fields`, not `for f in fields`.78 Applies in comprehensions too: `{field.name: field for field in fields}`.7980## Docstrings (Google style)8182```python83async def fetch_user(io: Io, user_id: str) -> User | None:84 """Fetch user from database by ID.8586 The returned User is a snapshot; mutations won't persist.8788 Args:89 io: I/O capability handle.90 user_id: The unique identifier for the user.9192 Returns:93 The User object if found, None otherwise.9495 """96```9798- Imperative mood: "Fetch", not "Fetches"99- Args section if function takes parameters100- Returns section always (even for None)101- Blank line before closing `"""`102- Full sentences with periods. No dashes or sentence fragments.103104## Comments105106- Explain WHY, not WHAT107- Delete narration: `user = get_user(id) # Get the user` → delete the comment108- Document trade-offs and policy choices109- Section headers: single-line `# --- Label ---` (no multi-line box separators)110- Column-align inline field comments with `# fmt: off` / `# fmt: on`111 (see column-aligned-fields skill for detailed rules)112113## Module Organization114115- `__init__.py`: bare docstring only. No imports, no re-exports, no `__all__`.116- No `__all__` in implementation files either. Public API is communicated by117 naming convention (public vs `_`-prefixed). We never use `import *`.118- One class per stage file. Class absorbs all logic (not a thin wrapper).119- Prefer public methods. Only `_`-prefix true internals callers never need.120- Types (dataclasses, aliases) in `types.py`. Logic in the main module.121- No backward-compat shims, convenience wrappers, or `_default_instance` patterns.122- Use `functools.cache` for expensive I/O loads, not manual `ClassVar[dict]`123 caches. Don't import cross-package deps for simple stdlib needs.124125## Constants126127- Use Enum or frozen dataclass, not raw dicts/lists128- Numeric literals > 999 need underscores: `100_000` not `100000`129- Powers of 2 are exempt: `4096` is fine130131## Testing (Inline)132133```python134# --- Tests ---135136from inline_tests import test # noqa: E402137138139@test140async def rejects_empty_id():141 import pytest # noqa: PLC0415142143 with pytest.raises(ValueError):144 await fetch_user("")145```146147- Colocated with implementation148- Local imports with `# noqa: PLC0415`149- One test = one scenario150- Unit tests (synthetic data) inline, integration tests (real files) in `tests/`151- Tests verify behavioral contracts, not implementation details or Python builtins152- Run: `uv run pytest path/to/file.py --inline-tests -v`153154## Before Committing155156- `uv run ruff format <files>`157- `uv run ruff check <files> --fix`158- Each file <= 500 production lines159- Each function <= 70 lines160- No `Any` without justification161- No `__all__` in any implementation file