references/code.md
Naming in code
Functions
A function name is a verb phrase describing the effect, precise enough that a reader never has to open the body to know what calling it does.
# vague: what does it do, and does it write anything?
def process_order(order): ...
def handle_bid(bid): ...
# precise
def settle_order(order): ...
def reject_bid_below_reserve(bid): ...
Encode the surprising part. A caller must be able to see side effects, cost, and failure mode from the call site:
| Name | Tells the reader |
|---|---|
get_user(id) | cheap, in memory, always succeeds or returns None |
fetch_user(id) | crosses the network, can fail, can be slow |
load_user(id) | reads from disk or database |
ensure_user(id) | may create as a side effect |
try_parse_date(s) | returns a failure rather than raising |
Once you pick a verb for a meaning, it means that everywhere. Mixing get and fetch for the same operation forces a lookup at every call site.
Consistent verbs, house set:
get (cheap lookup), fetch (remote), load (storage), list (many), create, update, delete, ensure (idempotent create), build (pure construction), validate (raises or returns errors), is/has/can (predicates), to (conversion), on (event handler).
Argument order and count. More than three positional arguments means the call site is unreadable, and it is usually a missing type:
create_order(user_id, vehicle_id, amount_minor, currency, address_id, notes) # what is what
create_order(OrderDraft(...)) # one thing
Booleans
Assertions, positively phrased, answerable with yes:
is_active has_balance can_refund should_retry was_settled
Never store or name a negative. not_ready, disable_cache, hide_footer produce double negatives at the call site, and double negatives are read wrong under time pressure.
if not not_ready: # wrong every time
if is_ready: # right
The exception is a flag whose default must be true: allow_retry=True reads better than forbid_retry=False. Even then, keep it positive.
Variables
Length scales with scope and lifetime.
for i, row in enumerate(rows): # fine: 3 lines, obvious
...
settlement_deadline_utc = ... # module level: spell it out
No type in the name. user_list, name_str, config_dict restate what the type system already says, and go stale the moment the type changes. users, name, config.
The exception is units, which the type system does not capture and which cause real bugs. Always name them:
price_minor timeout_ms size_bytes distance_km ttl_seconds
price is ambiguous between 15.00 and 1500. price_minor is not. If your codebase inherits one convention for money, state it once at the top of the data layer and use the suffix everywhere.
No single-letter names outside tight loops or accepted maths (x, y, n, i, j). d for a document, u for a user, r for a response are how a function becomes unreadable at 200 lines.
Avoid the temporary trio: data, info, result, temp, obj, item. Each of them means "I did not want to think of a name". payload, bid_response, settled_orders cost nothing and save a read.
Classes and types
Noun phrases for what it is, or what it does if it is a service:
Auction AuctionSettler BidValidator PriceCalculator
Not AuctionManager, BidHelper, PriceUtils. Those names appear when a class has no single responsibility, so the name is a design signal: if you cannot name it without Manager, split it.
No prefixes. No IUserRepository in TypeScript, no CUser, no _private class names. The language already knows.
Suffixes that carry meaning are fine, when they are a real pattern and used consistently: ...Repository, ...Error, ...Config, ...Client, ...Job.
Errors end in Error and say what failed:
class InsufficientDepositError(DomainError): ...
class AuctionAlreadySettledError(DomainError): ...
Not AuctionError for nine different failures, since the caller then has to inspect a message string to decide what to do.
Files and modules
Python: snake_case.py, named for the domain, singular or plural to match the contents.
bank_accounts.py auctions.py settlement.py
TypeScript, non-component: kebab-case.ts.
skill-builder.ts install-script.ts use-paged-list.ts
React components: PascalCase.tsx, matching the exported component exactly. One component per file for anything exported.
ResourceTable.tsx -> export function ResourceTable()
This is the one place where file casing differs, and it is worth it: the import statement and the JSX tag then read identically, and a mismatch between file name and export is immediately visible.
Do not repeat the folder in the file. auctions/auctions_service.py reads as auctions.auctions_service. Use auctions/service.py.
Test files mirror the module under test: auctions.py gets test_auctions.py. When a test file grows past a few hundred lines, split by behaviour and say so in the name: test_auctions_settlement.py.
Constants and enums
Module constants are SCREAMING_SNAKE, and only when genuinely constant.
MAX_BID_MINOR = 10_000_000
DEFAULT_PAGE_SIZE = 50
Enum members are the domain word, lowercase in the wire format:
class OrderStatus(str, Enum):
draft = "draft"
paid = "paid"
settled = "settled"
Short strings, not integers. status: "paid" is greppable in logs and readable in a database shell; status: 3 requires a lookup table nobody has.
Smells
Names that reliably indicate a design problem, not just a style problem:
| Name | What it usually means |
|---|---|
XManager, XHelper, XUtils | No single responsibility. Split it. |
data, info, payload2 | The concept was never named. |
process(), handle(), run() | Does several things. Name each. |
flag, mode, type as a parameter | A boolean parameter that should be two functions. |
do_x_and_y() | The and is telling you it is two functions. |
utils.py growing past a few functions | A missing module, waiting to be named. |
temp_fix, new_, v2_ in a name | Version in the name. The old one should have been deleted. |
new_ and v2_ deserve emphasis: they are permanent. Two years later process_order_v2 is the only one in use, process_order is dead, and nobody dares delete either.