Honest Code Principles
Sixteen engineering positions. Each names a pattern, states why the conventional alternative fails, and shows the honest version.
class UserService:
def __init__(self, db, config):
self._db = db # hidden state
self._config = config # hidden state
def create_user(self, email: str, role: str):
if role == "admin": # dispatch chain
self._db.grant_admin() # I/O in method
elif role == "editor":
self._db.grant_editor()
user = User(email=email, role=role)
self._db.save(user) # mutation + I/O
return user
ROLE_GRANTS = {
"admin": grant_admin, # dict dispatch (P01)
"editor": grant_editor,
}
def create_user(manifest: dict) -> dict: # pure fn (P03)
return {
**manifest,
"user": {"email": manifest["email"],
"role": manifest["role"]},
"grant": ROLE_GRANTS[manifest["role"]],
}
# No class. No self. No hidden state.
# I/O happens at the boundary, not here.
class OrderService
def initialize(order_repo, mailer)
@order_repo = order_repo # hidden state
@mailer = mailer # hidden state
end
def create(params)
order = Order.new(params) # mutation
order.save! # I/O in method
@mailer.send_confirmation(order) # I/O in method
order
end
end
def build_order(manifest) # pure function
manifest.merge(
order: {
id: SecureRandom.uuid,
items: manifest[:items],
total: manifest[:total],
}
)
end
# Boundary link handles I/O:
# - save order (honest-persist)
# - emit order.placed (honest-observe)
# - send confirmation (honest-alerts)
type OrderService struct {
db *sql.DB // hidden state
mailer Mailer // hidden state
}
func (s *OrderService) Create(params map[string]any) (*Order, error) {
order := &Order{Items: params["items"]} // mutation
if err := s.db.Save(order); err != nil { // I/O in method
return nil, err
}
s.mailer.Send(order) // I/O in method
return order, nil
}
// Pure function — no receiver, no I/O
func BuildOrder(manifest Manifest) Manifest {
return Manifest{
"order": map[string]any{
"id": uuid.New().String(),
"items": manifest["items"],
"total": manifest["total"],
},
}
}
// Boundary link calls SaveOrder(conn, manifest)
// and EmitOrderPlaced(manifest) — I/O at the edges.
class OrderService {
public function __construct(
private OrderRepository $repo,
private MailService $mailer,
) {}
public function create(array $data): Order {
$order = new Order($data); // mutation
$this->repo->save($order); // I/O in method
$this->mailer->confirm($order); // I/O in method
return $order;
}
}
function buildOrder(array $manifest): array {
return array_merge($manifest, [
'order' => [
'id' => (string) Str::uuid(),
'items' => $manifest['items'],
'total' => $manifest['total'],
],
]);
}
// Pure function. No class. No $this. No I/O.
// Boundary link handles save + notify.
# Class with hidden state and mutating methods.
# I/O inside business logic methods.
# if/elif dispatch chains.
# Inheritance for code reuse.
# Tests need mocks to isolate the I/O.
# The class is the unit. The test is the mock setup.
# Pure functions that take data in and return data out.
# I/O only at declared boundaries.
# Dict dispatch tables instead of if/elif chains.
# TypedDict over classes.
# No mocks — the pure function has nothing to mock.
# The test is: assert f(input) == expected_output.
The concept
Honest Code is not a style guide. It is a set of engineering positions backed by specific claims about what makes software fail at scale. Each principle names a pattern, states why the conventional alternative fails, and shows what the honest version looks like.
The sixteen principles
Dict-lookup polymorphism. An if/elif/else chain that dispatches on type or category is a data table masquerading as control flow. Replace it with a dict mapping keys to handlers. Adding a new case means adding a row, not modifying logic. The dict is the honest representation of the dispatch table that was already there.
Typed dicts over classes. A class with fields, methods, getters, setters, and lifecycle hooks is a data structure that refuses to admit what it is. A TypedDict is the same data with no behavior attached. If you cannot json.dumps() it, it is too clever by half.
Pure functions over methods. A method that mutates internal state hides its side effects behind a noun. A pure function takes data in and returns data out. No self. No side effects. No surprises. The function is testable with a single assertion.
I/O at the boundary. Pure business logic in the middle; I/O at the edges. The boundary calls the pure function and does the I/O with the result. This is why mocks become unnecessary — the pure core has nothing to mock.
Flat composition over inheritance. pipe(validate, authenticate, rate_limit, create_order) is visible at the point of assembly. class B extends A extends Base hides the execution path. No super() calls. No method resolution order surprises.
DOM as state. The DOM is the state. The server renders HTML; HTMX swaps it into the page. Server-rendered fragments replace client-side state management. One copy of truth, not two.
HTML attributes over imperative DOM manipulation. The attribute declares intent; the library handles mechanism. hx-post="/endpoint" and hf-format="currency" replace seventy-three lines of addEventListener, querySelector, and innerHTML.
Typed exceptions at the boundary. Let functions raise. The route handler catches, inspects the exception type, and returns the appropriate status code. Retry logic belongs in infrastructure, not inline in the function.
SQL over application caches. A single SQL join with proper indexes runs under 3ms. The cache adds invalidation bugs, stale data, and a second source of truth. Profile the query first. Only cache after measurement proves it necessary.
Pure function assertions over mocks. assert f(input) == expected_output. That is the whole test. If you need nine mocks to test a function, the function has nine hidden dependencies. Extract the pure logic and test it directly.
Type declarations over imperative validation. Declare a schema, a TypedDict, a SQL column constraint, or an <input type="email">. The runtime, type checker, database, or browser enforces it. The programmer declares; the machinery enforces.
Context managers over instance state. async with create_connection(config) as conn: opens and closes within the scope. No persistent state leaks into the caller. Crash recovery is trivial because there is nothing to clean up.
Configuration as parameters. Pass config: dict as an argument. The dependency is visible in the signature. No hidden state. No initialization order bugs.
Simple Gherkin steps signal honest architecture. If your step definition is thirty lines of mock configuration, the code under test has hidden dependencies. When the function is pure, the step is: call the function, check the result.
Declarative equivalents over lifecycle hooks. hx-trigger="load" replaces componentDidMount. Server-rendered HTML arrives ready. No client-side initialization sequence.
Strangler pattern for migration. Extract one pure function from one class method per sprint. The class still exists; the interface does not change. After six months the class is a thin shell. Removing it is a cleanup task.
The abstract principle
Honest code is not a style. It is a set of structural properties with mechanical consequences.
The sixteen principles are not aesthetic preferences. Each one names a structural pattern, states the mechanical consequence of the dishonest version, and shows the honest version. The honest version is always measurably easier to verify, test, and reason about. Not subjectively cleaner. Measurably better on specific dimensions.
The core claim
Big State is the disease. Classes are the primary vector.
A class with fields, methods, getters, setters, and lifecycle hooks combines data and behavior in one object. The data is hidden behind the interface. The behavior has side effects on the hidden data. The side effects are not visible at the call site. The hidden state accumulates across calls. The program is a network of objects mutating each other's hidden state.
This is the standard object-oriented model. It is also why large OOP codebases are difficult to understand, difficult to test, and prone to bugs that appear only under specific sequences of method calls.
A pure function has no hidden state. It takes data in and returns data out. Every input is visible at the call site. Every output is visible at the return. The program is a composition of functions. Understanding any part of it requires only reading that function and the functions it calls.
Why dict dispatch is theoretically superior
An if/elif/else chain is an incomplete function. It can have cases that fall through. It can have unreachable branches. It can have implicit defaults. None of these properties are verifiable without exhaustive testing.
A dict dispatch table is a partial function with a declared domain. The keys are the domain. Every key either has a handler or it does not. This is checkable by set inspection: assert set(FEATURES.keys()) == set(HANDLER_TABLE.keys()). The check is O(n), deterministic, and requires no execution.
This is the formal argument for dict dispatch, not just the practical one: a dict is a verifiably total (or partial) function. An if/elif/else chain is a partial function with no verifiable domain declaration.
Referential transparency
A pure function has the property of referential transparency: the function call can be replaced by its return value without changing the program's meaning. This is the mathematical definition of a pure function.
Referential transparency enables equational reasoning: you can substitute equals for equals in your mental model of the program. You can understand a composition of pure functions by understanding each function in isolation and combining the understanding. You cannot do this with methods that have side effects on shared state.
honest-test's purity verification checks referential transparency empirically: run the function twice with the same input. If the outputs differ, the function is not referentially transparent. This is not a proof. It is a strong practical check that catches the most common purity violations.
The cost accounting argument
The Honest Code benchmark data (honestcode.software/evidence.html) shows that honest code is faster than dishonest code on the same hardware. Python dishonest 1,916ns vs honest 1,417ns. Java mock-heavy 1,376ns vs pure function 334ns (4.1x).
The performance argument is secondary to the correctness argument, but it resolves a common objection. The objection is: "functional style is cleaner but slower." The benchmark data shows this is false. Pure functions doing pure computation with no pointer-chasing, no vtable dispatch, no heap scatter, and no synchronization overhead are faster, not slower.
The CPU performs best doing pure computation. Hidden state, synchronization, and object indirection are not free. They are costs paid for no architectural benefit.
Full specification
The Sixteen Principles
P01 — Dict-Lookup Polymorphism
An if/elif/else chain that dispatches on type or category is a data table masquerading as control flow. Replace it with a dict mapping keys to handlers. Adding a case means adding a row, not modifying logic. honest-check HC-P001 enforces this statically. The deciding rule: if the condition's values are enumerable, the pattern applies.
Two named sub-patterns:
- Data table: same operation, varying values — {"USD": 1.0, "EUR": 0.85, "GBP": 0.73}
- Handler table: different operations per key — {"email": send_email, "sms": send_sms}
P02 — Typed Dicts Over Classes
A class with fields, methods, getters, setters, and lifecycle hooks is a data structure that refuses to admit what it is. Replace it with a TypedDict (or language equivalent). The data is just data. If you cannot json.dumps() it, it is too clever by half. honest-check HC-P002 and HC-P003 enforce this statically.
P03 — Pure Functions Over Methods
A method that mutates internal state hides its side effects. A pure function takes data in and returns data out. No self. No side effects. No surprises. A pure function is testable with a single assertion: assert f(input) == expected. honest-check HC-P002 enforces this.
P04 — I/O at the Boundary
Pure business logic in the middle; I/O at the edges. The boundary calls the pure function and does the I/O with the result. This is why mocks become unnecessary — the pure core has nothing to mock. honest-check HC-P004 enforces this. The @link decorator declares which functions are boundaries.
P05 — Flat Composition Over Inheritance
pipe(validate, authenticate, rate_limit, create_order) is visible at the point of assembly. class B extends A extends Base hides the execution path. No super() calls. No method resolution order surprises. honest-check HC-P003 enforces this.
P06 — DOM as State (DATAOS)
The DOM is the state. The server renders HTML; HTMX swaps it into the page. hx-get and hx-target replace useState and useEffect. One copy of truth, not two. honest-check HC-P011 enforces this.
P07 — HTML Attributes Over Imperative DOM Manipulation
The attribute declares intent; the library handles mechanism. hx-post="/endpoint" and hf-format="currency" replace addEventListener, querySelector, and innerHTML. Seventy-three lines of JavaScript become six attributes.
P08 — Typed Exceptions at the Boundary Let functions raise. The route handler catches, inspects the exception type, and returns the appropriate HTTP status. Retry logic belongs in infrastructure, not inline in the function.
P09 — SQL Over Application Caches A single SQL join with proper indexes runs under 3ms. The cache adds invalidation bugs, stale data, and a second source of truth. Profile the query first. Cache only after measurement proves it necessary. honest-check HC-P006 flags caches without profiling evidence.
P10 — Pure Function Assertions Over Mocks
assert f(input) == expected. That is the whole test. If you need nine mocks to test a function, the function has nine hidden dependencies. Extract the pure logic and test it directly.
P11 — Type Declarations Over Imperative Validation
Declare a schema, a TypedDict, a SQL column constraint, or an <input type="email">. The runtime, type checker, database, or browser enforces it. The programmer declares; the machinery enforces.
P12 — Context Managers Over Instance State
async with create_connection(config) as conn: opens and closes within the scope. No persistent state leaks into the caller. Crash recovery is trivial because there is nothing to clean up. honest-check HC-P007 flags instance state in constructors.
P13 — Configuration as Parameters
Pass config: dict as an argument. The dependency is visible in the signature. No hidden state. No initialization order bugs.
P14 — Simple Gherkin Steps Signal Honest Architecture If your step definition is thirty lines of mock configuration, the code under test has hidden dependencies. When the function is pure, the step is: call the function, check the result.
P15 — Declarative Equivalents Over Lifecycle Hooks
hx-trigger="load" replaces componentDidMount. Server-rendered HTML arrives ready. No client-side initialization sequence. honest-check HC-P011 enforces this.
P16 — Strangler Pattern for Migration Extract one pure function from one class method per sprint. The class still exists; the interface does not change. After six months the class is a thin shell. Removing it is a cleanup task.
Statically Enforced Principles
| Principle | honest-check Rule |
|---|---|
| P01 Dict-lookup polymorphism | HC-P001 |
| P02 Typed dicts over classes | HC-P002, HC-P003 |
| P03 Pure functions over methods | HC-P002 |
| P04 I/O at the boundary | HC-P004 |
| P05 Flat composition over inheritance | HC-P003 |
| P06 DOM as state | HC-P011 |
| P07 HTML attributes | HC-P011 |
| P09 SQL over caches | HC-P006 |
| P12 Context managers | HC-P007 |
| P15 Declarative over lifecycle hooks | HC-P011 |
Principles P08, P10, P11, P13, P14, P16 are architectural positions verified by code review and honest-test, not by static rules.
Runtime Enforcement
honest-test's honesty tests verify P03 (purity), P04 (boundary isolation), and P13 (configuration as parameters) at runtime by instrumenting link execution and detecting I/O, mutations, and non-determinism automatically.
Conformance
A conformant codebase satisfies all sixteen principles. The degree of automated enforcement depends on the tooling available:
- Statically verifiable: P01, P02, P03, P04, P05, P06, P07, P09, P12, P15 — honest-check enforces these before commit
- Runtime verifiable: P03, P04, P10, P13 — honest-test verifies these in the test suite
- Architecturally verified: P08, P11, P14, P16 — verified through code review against this document
Reference
- honest-code-principles.md The sixteen principles
- Honest Code (the book) Python chapter — FastAPI patterns
- honestcode.software/evidence.html Benchmark data: Python dishonest 1,916ns vs honest 1,417ns