Start with the Caller
When I write a function, I try to think about how I want to call it from the outside before I write a single line of the body. That shift in perspective changed my code quality more than any linting rule or design pattern. The function signature is the contract, and the body is just the implementation. If the signature is awkward, the body will be too.
A good signature reads like a sentence. It tells you what goes in and what comes out without forcing you to read the implementation. For example:
# Bad: unclear what the return value represents
def process(data, flag):
...
# Good: the name and parameters tell the story
def build_report(transactions, include_totals=False):
...
This clarity compounds. Every function you write becomes a building block for the next one. If the building blocks are clean, the higher-level code becomes a pleasure to read.
One Job, One Level of Abstraction
You've heard "single responsibility" before, but it's easy to slip. The real test is whether you can describe the function in one sentence without using "and" or "then".
// Bad: does three things
function updateUserAndSendEmail(user, changes) {
// update db
// send email
// log
}
// Good: separate functions, each with one job
function applyUserChanges(user, changes) { /* ... */ }
function notifyUser(user) { /* ... */ }
Also, keep the body at a consistent level of abstraction. If your function is about business logic, don't drop down to string manipulation or low-level array indexing. Extract those into helpers. This makes the main flow read like a high-level summary.
Naming Is Design
Names are not just for humans; they guide future refactoring. A good name reveals intent and makes the function self-documenting.
- Use verbs for actions:
calculateTotal,fetchUser,validateInput. - Use nouns for things:
getUserByIdmight return a user, butuserByIdis ambiguous. - Avoid generic words like
data,info,stuff. They hide meaning.
Sometimes a name is hard because the function does too much. If you can't name it concisely, that's a signal to split it.
Defaults and Options That Don't Pollute
Optional parameters are fine, but they should not create hidden branches. I prefer to pass explicit options rather than booleans that flip behavior.
# Bad: boolean flag changes behavior drastically
def format(data, pretty=False):
if pretty:
return json.dumps(data, indent=2)
return json.dumps(data)
# Better: separate functions or an options object
import json
def format_compact(data):
return json.dumps(data)
def format_pretty(data):
return json.dumps(data, indent=2)
When you have many options, consider an options object or a configuration parameter. This keeps the signature stable and easy to extend.
Return Early, Return Often
Deeply nested conditionals are a sign of a function that's trying to do too much. I use guard clauses to handle edge cases first, then the main logic runs cleanly.
def get_discount(price, customer):
if not customer.is_member:
return 0
if price < 100:
return 0.05
return 0.1
This linear flow is easier to follow than nested if-else blocks. It also makes it obvious what the function returns in each case.
The Compounding Effect
When you write clean functions, you build a vocabulary. Later, you can compose them like LEGO bricks. The whole system becomes greater than the sum of its parts because each function is predictable and testable.
For example, a payment flow might look like:
def process_payment(order, payment_method):
amount = calculate_total(order)
charge = charge_customer(payment_method, amount)
send_receipt(order, charge)
return charge
Each of those helper functions is simple, and the main function reads like a story.
Start Small, Refactor Later
You don't need to design perfectly upfront. Write the first version, then refactor. But keep the bar high: if you see a function that's hard to test, hard to name, or has too many responsibilities, break it down.
Clean function design is a habit. The more you do it, the more natural it becomes. And the payoff is real: less debugging, easier onboarding, and code that you actually enjoy reading six months later.
So next time you write a function, ask yourself: would I want to call this from another function? Would I want to unit test it? If the answer is no, redesign it. That small discipline compounds into a codebase that's a joy to work with.
Top comments (0)