# Py Clean Functions

> Clean Functions

- Skill: `caslubbers/py-clean-functions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add caslubbers/py-clean-functions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/caslubbers/py-clean-functions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: CasLubbers (https://skillmd.com/u/caslubbers)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/caslubbers/py-clean-functions

---


# Clean Functions

## F1: Too Many Arguments (Maximum 3)

```python
# Bad - too many parameters
def create_user(name, email, age, country, timezone, language, newsletter):
    ...

# Good - use a dataclass or dict
@dataclass
class UserData:
    name: str
    email: str
    age: int
    country: str
    timezone: str
    language: str
    newsletter: bool

def create_user(data: UserData):
    ...
```

More than 3 arguments means your function is doing too much or needs 
a data structure.

## F2: No Output Arguments

Don't modify arguments as side effects. Return values instead.

```python
# Bad - modifies argument
def append_footer(report: Report) -> None:
    report.append("\n---\nGenerated by System")

# Good - returns new value
def with_footer(report: Report) -> Report:
    return report + "\n---\nGenerated by System"
```

## F3: No Flag Arguments

Boolean flags mean your function does at least two things.

```python
# Bad - function does two different things
def render(is_test: bool):
    if is_test:
        render_test_page()
    else:
        render_production_page()

# Good - split into two functions
def render_test_page(): ...
def render_production_page(): ...
```

## F4: Delete Dead Functions

If it's not called, delete it. No "just in case" code. Git preserves history.

## The Stepdown Rule: Code Reads Top-Down

A module should read like a narrative: the highest level of abstraction first, each function
followed by the ones it calls. The reader descends one level at a time and can stop as soon as
they know enough.

```python
# Bad - the reader meets encoding details before knowing what the module does
def _escape_quotes(value: str) -> str: ...
def _serialise_row(row: Row) -> str: ...
def export_orders(orders: list[Order]) -> str: ...   # the point, buried at the bottom

# Good - the story first, the details underneath
def export_orders(orders: list[Order]) -> str:
    return "\n".join(_serialise_row(o) for o in orders)

def _serialise_row(row: Row) -> str:
    return ",".join(_escape_quotes(field) for field in row)

def _escape_quotes(value: str) -> str:
    return value.replace('"', '""')
```

Two consequences follow:

**One level of abstraction per function.** A function mixing orchestration with string escaping has
no place in the ordering, because it belongs at two levels at once. That is the signal to split it.

```python
# Bad - policy and byte-level detail in one breath
def publish(post: Post) -> None:
    if post.author.is_banned: return
    body = post.body.replace("\r\n", "\n").strip()[:5000]
    db.execute("INSERT INTO posts ...", body)

# Good - each line is the same size of idea
def publish(post: Post) -> None:
    if _is_publishable(post):
        _store(post.author, _normalise(post.body))
```

**Vertical distance tracks relatedness.** Callee directly below caller. Variables declared close to
first use. Public functions before the private helpers they call. A reader should not scroll to
follow one thought.

If a file cannot be ordered this way, it holds more than one responsibility and wants splitting.

