naming-and-boundaries
verifiedc28edc1f-2a25-4c30-a039-7906d90b4971
Name functions, variables, and classes for intent and keep each unit single-responsibility. Use when code is hard to read or a function keeps growing.
Metadata
Skill file
# Naming and Boundaries
Use when code is hard to read, a function keeps growing, or you cannot tell what a
piece of code does from its name. Good names and small single-responsibility units
are the cheapest maintainability win available.
## 1. Naming rules
| Kind | Rule | Good | Bad |
|---|---|---|---|
| Function | verb phrase — what it *does* | `calculate_total()` | `total()` |
| Class | noun — what it *is* | `Invoice` | `ProcessInvoice` |
| Boolean | `is_`/`has_`/`can_` predicate | `is_expired` | `check()` |
| Collection | plural noun | `orders` | `order_list` |
| Constant | `UPPER_SNAKE_CASE` | `MAX_RETRIES` | `maxRetries` |
Banned names — they convey no information:
```python
data, info, tmp, temp, handle, do, process, manage, thing, stuff, util,
helper, manager, get (without an object), result (without qualification)
```
Replace each with the *specific* thing: `user_input` not `data`, `invoices` not
`stuff`, `fetch_user` not `get`.
## 2. The 10-line function heuristic
If a function exceeds ~10 lines (excluding docstring and blank lines), it is doing
more than one thing. **Extract until the intent reads as prose:**
```python
# BEFORE — 20 lines, three responsibilities, unclear intent
def handle(event):
# validate...
if not event.get("id"):
return 400
# fetch...
user = db.query(User).filter_by(id=event["id"]).first()
# notify...
send_email(user.email, "welcome")
return 200
# AFTER — intent reads as prose
def handle(event):
if not is_valid(event):
return 400
user = fetch_user(event["id"])
notify_welcome(user)
return 200
```
The test: read the function body aloud. If it reads like a sequence of clear
sentences, you have the right boundaries.
## 3. Parameters-as-a-smell
More than 3 positional arguments is a signal that parameters belong together:
```python
# SMELL — 5 positional args, easy to mis-order
def create_user(name, email, age, country, plan, is_admin): ...
# FIX — group into a dataclass/dict
@dataclass(frozen=True)
class NewUser:
name: str
email: str
age: int
country: str
plan: str
is_admin: bool = False
def create_user(user: NewUser) -> User: ...
```
Also prefer **keyword-only** args for optional parameters to prevent mis-ordering:
```python
def send(to: str, *, cc: list[str] = [], retries: int = 3): ...
```
## 4. Extracting — the mechanics
Use your IDE's refactor, never `sed`:
```bash
# PyCharm: Ctrl+Alt+M (extract method), Shift+F6 (rename)
# VS Code: Ctrl+. -> "Extract to function" / "Rename Symbol"
# Vim/CLI: rope or jedi rename, or do it manually + fix callers by grep
```
After a rename, grep for stale callers and run the tests:
```bash
rg 'old_name' . # find any caller you missed
pytest -q # prove behavior is unchanged
```
## 5. Renaming in a large codebase (migration playbook)
Renaming a symbol that has many callers is a mechanical, safe operation if you
sequence it right:
```bash
# 1. Find every reference first
rg -n 'old_name' src/ tests/
# 2. Rename via IDE refactor (or a single careful sed if no IDE)
# PyCharm: Shift+F6 | VS Code: F2 | vim: :%s/\bold_name\b/new_name/gc
# 3. Confirm zero stale references remain
rg -n '\bold_name\b' src/ tests/ # expect: no output
# 4. Prove behavior unchanged
pytest -q
# 5. Commit the rename alone (no behavior change mixed in)
git commit -m "Rename old_name -> new_name"
```
The discipline: rename in its own commit, verify with `rg` + tests, and never mix
a rename with a behavior change (that makes the diff unreviewable).
## 6. Boundaries beyond functions — modules and classes
Single responsibility applies at every level, not just functions:
| Level | "One thing" means | Splits when |
|---|---|---|
| Function | One operation | > ~10 lines or "and" in the name |
| Class | One concept + its data | Two unrelated field groups |
| Module | One cohesive set of related functions/classes | Importing it drags in unrelated deps |
A module named `utils.py` or `common.py` is a boundary failure — it has no "one
thing." Split it into purpose-named modules (`time_utils.py`, `string_utils.py`,
`http_client.py`).
## Guardrails
- Do **not** rename with `sed`/find-replace — it misses dynamic references and
breaks callsites. Use IDE refactor or verify with `rg` + tests.
- Do **not** use vague-but-short names (`f`, `x`, `val`) just to save typing — the
reader pays for it.
- Do **not** substitute comments for names. If you need `# x is the user count`,
rename `x` to `user_count`.
- Do **not** extract so aggressively that you create 5-line wrappers with no
independent meaning.
- Keep one responsibility per function/class — if the name needs "and" ("fetch
*and* save"), it is two functions.
## Pitfalls
- **Renaming without updating callers** — the top source of "it worked yesterday"
breakage; always `rg` + run tests after a rename.
- **Vague-but-short names** — `d`, `e`, `res` are worse than slightly longer
descriptive names.
- **Comments as name-substitutes** — a comment explaining `data` should be a better
variable name.
- **Over-extraction** — 30 one-line functions is not readable, it is fragmentation.
- **`util`/`helper`/`manager` junk drawers** — names that mean nothing attract
everything; split them by responsibility.
## Verify / Checklist
- [ ] Every function is a verb phrase; every class is a noun; booleans are `is_`/`has_`/`can_`.
- [ ] No banned names (`data`, `info`, `tmp`, `handle`, `util`, `manager`) without strong justification.
- [ ] No function exceeds ~10 lines of logic (excluding docstring/blank lines).
- [ ] No function/method has more than 3 positional parameters.
- [ ] A rename was followed by `rg old_name` to confirm no stale callers.
- [ ] Tests pass after any rename/extract (behavior is unchanged).
- [ ] Reading a function body aloud produces clear, discrete sentences.
Attached files
No attached files.