A compact checklist distilled from Varyonic programming and Designing for mutation. Give this page (or its contents) to an LLM coding assistant as part of its system prompt or project instructions.
Return structured data
| Do | Don't |
Return data types with named fields | Return Bool or None for success/failure |
| Return report objects with counts and lists | Print details to stdout |
Return Result[T, E] for fallible ops | Call exit() from library code |
# Good
data CheckReport {
success: Bool
issues: List[Str]
files_checked: List[Str]
}
def check_site(pages: List[Str]) -> CheckReport { ... }
# Bad
def check_site(pages: List[Str]) -> Bool { ... }
Use domain types
| Do | Don't |
Define enum for closed choices | Use magic strings ("prod") |
Define data for domain concepts | Use Dict[Str, Any] |
Use Str? for optional values | Use "" as a sentinel for "missing" |
# Good
enum DeployTarget { Local, Preview, Production }
# Bad
def deploy(mode: Str) { if mode == "prod" { ... } }
Write pure functions for core logic
| Do | Don't |
Mark decision logic pure def | Mix I/O and logic in one function |
| Put validation and planning in pure functions | Read config inside core logic |
| Return a plan; execute it separately | Perform effects inside decision logic |
# Good
pure def plan_build(sources: List[Str]) -> BuildPlan { ... }
def run_build(plan: BuildPlan) -> BuildReport { ... }
# Bad
def build(sources: List[Str]) {
for s in sources { fs.write_text(fs.write_path(s + ".out").unwrap(), compile(s)).unwrap() }
}
Separate decisions from effects
| Do | Don't |
| Inputs -> planning -> execution -> report | One function that reads, decides, acts, prints |
| Keep effectful functions thin | Put print() or exit() in planning logic |
Limit exit() to the CLI entry point | Call exit() from library functions |
Test with observe, not just success checks
| Do | Don't |
observe report.issues.len() == 0 | observe result == true |
observe report.files_checked.len() == 5 | observe report is not None |
| Assert every important field | Assert only that no error occurred |
| Test relationships between outputs | Test only one output in isolation |
# Good
test "plan has correct fields" {
let plan = plan_deploy("prod", "/var/www/site")
observe plan.strategy == DeployStrategy.Rsync
observe plan.source_dir == "build/"
observe plan.destination == "/var/www/site"
}
# Bad
test "plan works" {
let plan = plan_deploy("prod", "/var/www/site")
observe plan is not None
}
Test negative paths
| Do | Don't |
| Test empty lists, bad input, boundaries | Only test the happy path |
| Test "almost valid" input | Assume bad input never arrives |
Test every Result.Err branch | Leave error branches unexercised |
For every important function, test at least: normal success, obvious invalid input, an edge case, a boundary condition, and "almost valid" input.
Use enums and match for finite decisions
| Do | Don't |
match kind { case PageKind.Doc { ... } } | if kind == "doc" { ... } |
| Cover every enum variant | Leave a catch-all else hiding cases |
Use contracts for invariants
| Do | Don't |
Add in { } preconditions | Silently accept invalid arguments |
Add out (result) { } postconditions | Hope the caller checks the result |
Use old(expr) for before/after state | Rely on manual inspection |
def parse_doc(md: Str) -> DocPage {
in { len(md) > 0 }
out (result) { result.slug != "" }
...
}
Use Result for errors, not exceptions
| Do | Don't |
def load(path: Str) -> Result[Doc, Str] | def load(path: Str) -> Doc that raises |
Propagate with ?else return Err(...) | Catch and swallow exceptions |
Return Err("descriptive message") | Return None for "went wrong" |
Extract small helpers
| Do | Don't |
Extract: normalize_link(), classify_change() | Inline 20 lines inside a loop body |
| Name helpers by one responsibility | Create a utils.vary grab-bag |
| Keep helpers independently testable | Abstract one-time operations |
Use defer for resource cleanup
| Do | Don't |
defer { fs.remove_dir(fs.path(dir)).unwrap() } after acquiring | Clean up manually at function end |
One defer per resource, right after creation | Forget cleanup on early-return paths |
Module organization
| Do | Don't |
| Split types, logic, and effects into files | Put everything in one file |
Name by responsibility: check_plan.vary | Name generically: helpers.vary |
Keep main.vary as a thin CLI shell | Put business logic in main.vary |
Anti-patterns to avoid
These patterns cause mutation survivors and weak test coverage:
| Pattern | Fix |
| Magic strings instead of enums | Define an enum |
Functions returning None silently | Return structured data or Result |
| Side effects mixed with decisions | Separate pure planning from execution |
| Logging instead of structured reports | Return a report data type |
| Tests asserting string fragments | Assert on typed fields |
Bare try/except with no re-raise | Use Result or re-raise |
| Large functions mixing plan and execute | Split into plan + execute |
Dict instead of domain models | Define data types |
| Tests that only check "no error" | Assert exact output values |
Quick reference
Program structure:
CLI -> Load context -> Pure planning -> Execution layer -> Structured report -> CLI prints + exit
Test structure:
test "descriptive name" {
let result = function_under_test(inputs)
observe result.field1 == expected1
observe result.field2 == expected2
observe result.count == result.items.len() # relational check
}
File layout:
types.vary # data and enum definitions
logic_plan.vary # pure planning functions
logic_execute.vary # effectful execution
main.vary # thin CLI entry point