Goal: Learn how to write proper functions that are easy to read, test, and reuse.
π€ What Makes a Function Good?
Bad function:
def f(x, y):
print("Result:")
z = x + y
print(z)
return z
Good function:
def calculate_sum(first_number, second_number):
"""Calculate the sum of two numbers."""
return first_number + second_number
The difference:
- β
Descriptive name
- β
Descriptive parameter names
- β
Documentation (docstring)
- β
Only computation, no output
- β
Simple and predictable
π Function Writing Rules
π Rule #1: The Name Should Say WHAT the Function Does
β Bad Names
def f(): # What does "f" do?
def data(): # What about data?
def process(): # What are we processing?
def do_stuff(): # What "stuff"?
def x(): # What is this?
Problem: You have to read the code inside just to understand what it’s for.
β Good Names
def calculate_total_price():
def send_email():
def validate_user_input():
def find_max_value():
def save_to_database():
Principle: The name is a verb + what we’re doing.
π― Naming Patterns
| Action | Examples |
|---|---|
| Get | get_user(), get_balance(), get_settings() |
| Set | set_name(), set_balance(), set_status() |
| Calculate | calculate_total(), calculate_tax(), calculate_discount() |
| Create | create_account(), create_user(), create_report() |
| Validate | validate_email(), check_balance(), is_valid() |
| Find | find_user(), search_products(), locate_file() |
| Save | save_data(), save_to_file(), save_user() |
| Load | load_data(), load_from_file(), load_settings() |
| Delete | delete_user(), remove_item(), clear_cache() |
| Update | update_balance(), refresh_data(), sync_data() |
π Special Prefixes
is_ / has_ / can_ β for boolean functions:
def is_adult(age):
"""Check if the user is an adult."""
return age >= 18
def has_permission(user):
"""Check if the user has the required permissions."""
return user.role == "admin"
def can_withdraw(balance, amount):
"""Check if the withdrawal amount is available."""
return balance >= amount
Return: True or False
π Rule #2: One Function β One Job (SRP)
SRP (Single Responsibility Principle) β a function should do exactly one thing.
β Bad: Function That Does Everything
def process_user(name, age):
print(f"Processing user {name}...")
if age < 18:
print("Minor!")
return False
with open("users.txt", "a") as f:
f.write(f"{name},{age}\n")
print("Sending email...")
# send email
print("Writing to DB...")
# write to DB
print("Done!")
return True
Problems:
- π΄ Does 4 different things
- π΄ Hard to test
- π΄ Parts can’t be reused
- π΄ Hard to understand what’s happening
β Good: Break It into Small Functions
def is_adult(age):
"""Check if the user is an adult."""
return age >= 18
def save_user_to_file(name, age):
"""Save the user to a file."""
with open("users.txt", "a") as f:
f.write(f"{name},{age}\n")
def send_welcome_email(name):
"""Send a welcome email."""
# send email
pass
def save_user_to_database(name, age):
"""Save the user to the database."""
# write to DB
pass
def register_user(name, age):
"""Register a new user."""
if not is_adult(age):
return False
save_user_to_file(name, age)
send_welcome_email(name)
save_user_to_database(name, age)
return True
Benefits:
- β
Each function does one thing
- β
Easy to test individually
- β
Can be reused
- β
Easy to read and understand
π Rule #3: Functions Should NOT Print (Pure Functions)
β Bad: Function with print Inside
def calculate_discount(price, discount_percent):
discount = price * discount_percent / 100
final_price = price - discount
print(f"Price: {price}")
print(f"Discount: {discount}")
print(f"Total: {final_price}")
return final_price
Problems:
- π΄ Can’t use it without side-output
- π΄ Hard to test
- π΄ Violates the single responsibility principle
- π΄ Mixes logic and display
β Good: Separate Logic and Display
def calculate_discount(price, discount_percent):
"""Calculate the discounted price."""
discount = price * discount_percent / 100
final_price = price - discount
return final_price
# Display separately
def show_discount_info(price, discount_percent):
"""Show discount details."""
final_price = calculate_discount(price, discount_percent)
discount = price - final_price
print(f"Price: {price}")
print(f"Discount: {discount}")
print(f"Total: {final_price}")
# Usage
price = calculate_discount(1000, 20) # Pure calculation
show_discount_info(1000, 20) # With output
Benefits:
- β
calculate_discount() is a pure function
- β
Can be used in tests
- β
Can be used without output
- β
Logic is separated from display
π― Pure Functions
A pure function:
1. Returns a result (doesn’t print)
2. Doesn’t modify external state (no side effects)
3. Always returns the same result for the same arguments
β Pure Functions
def add(a, b):
"""Add two numbers."""
return a + b
def calculate_area(width, height):
"""Calculate the area."""
return width * height
def is_even(number):
"""Check if a number is even."""
return number % 2 == 0
Characteristics:
- Only return, no print
- Don’t modify variables outside the function
- Predictable
β Impure Functions
total = 0
def add_to_total(value):
"""Add to the global variable."""
global total
total += value # Modifies external state!
def print_greeting(name):
"""Print a greeting."""
print(f"Hello, {name}!") # Prints instead of returning!
def get_random_number():
"""Get a random number."""
import random
return random.randint(1, 100) # Different result every time!
Why impure:
- Modifies external data (global)
- Prints instead of returning
- Depends on randomness/time
π Rule #4: Parameters Should Be Descriptive
β Bad
def f(x, y, z):
return x + y * z
result = f(10, 5, 2) # Which is which?
β Good
def calculate_total_price(base_price, quantity, tax_rate):
"""Calculate the total price including tax."""
return base_price + quantity * tax_rate
result = calculate_total_price(
base_price=10,
quantity=5,
tax_rate=2
)
Benefits:
- β
Clear what we’re passing in
- β
Order can be changed
- β
Self-documenting code
π Rule #5: Return Results, Don’t Mutate Arguments
β Bad: Mutating an Argument
def add_item(items, new_item):
"""Add an item to the list."""
items.append(new_item) # Modifies the original list!
my_list = [1, 2, 3]
add_item(my_list, 4) # my_list is now [1, 2, 3, 4]
Problem: Unexpected modification of external data!
β Good: Return a New Value
def add_item(items, new_item):
"""Create a new list with the added item."""
return items + [new_item] # Return a new list
my_list = [1, 2, 3]
new_list = add_item(my_list, 4) # my_list stays [1, 2, 3]
print(new_list) # [1, 2, 3, 4]
Benefit: The original data is not modified.
π Rule #6: Use Default Parameter Values
def create_account(name, balance=0, currency="USD"):
"""Create a bank account."""
return {
"name": name,
"balance": balance,
"currency": currency
}
# Different ways to call it
account1 = create_account("Alice")
account2 = create_account("Bob", 1000)
account3 = create_account("Charlie", 500, "EUR")
Benefits:
- β
Fewer required arguments at call sites
- β
Sensible defaults
- β
Flexible usage
π Rule #7: Document Your Functions (Docstrings)
β Without Documentation
def calc(p, d):
return p - (p * d / 100)
Problem: Unclear what it does, what p and d are.
β With Documentation
def calculate_discounted_price(price, discount_percent):
"""
Calculate the price after applying a discount.
Args:
price: The original item price
discount_percent: The discount percentage (0-100)
Returns:
The price after the discount is applied
Example:
>>> calculate_discounted_price(1000, 20)
800.0
"""
discount = price * discount_percent / 100
return price - discount
Docstring format:
1. Brief description (one line)
2. Extended description (if needed)
3. Args: parameters
4. Returns: what it returns
5. Example: usage example
π Rule #8: Don’t Make Functions Too Long
β Bad: A Function That’s Way Too Long
def process_order(order_id):
# 100+ lines of code
# order validation
# price calculation
# inventory check
# send email
# write to DB
# update stats
# logging
# ...
pass
Problem: Hard to read, test, and understand.
β Good: Break Into Small Functions
def validate_order(order_id):
"""Validate the order."""
# ...
def calculate_order_total(order):
"""Calculate the total amount."""
# ...
def check_inventory(order):
"""Check product availability."""
# ...
def send_confirmation_email(order):
"""Send a confirmation email."""
# ...
def save_order_to_database(order):
"""Save the order to the database."""
# ...
def process_order(order_id):
"""Process the full order."""
order = validate_order(order_id)
total = calculate_order_total(order)
if not check_inventory(order):
return False
save_order_to_database(order)
send_confirmation_email(order)
return True
Rule: A function should fit on one screen (~20-30 lines max).
π Rule #9: Return Meaningful Results
β Bad: Uninformative Return
def save_user(name):
# saving...
return True # What does True mean?
β Good: Informative Return
def save_user(name):
"""Save a user."""
try:
# saving...
return {"success": True, "message": "User saved"}
except Exception as e:
return {"success": False, "message": f"Error: {e}"}
result = save_user("Alice")
if result["success"]:
print(result["message"])
Or better yet β use exceptions:
def save_user(name):
"""Save a user. Raises an exception on failure."""
# saving...
# if error: raise Exception("...")
try:
save_user("Alice")
print("Saved!")
except Exception as e:
print(f"Error: {e}")
π Rule #10: Avoid Too Many Parameters
β Bad: 7+ Parameters
def create_user(name, age, email, phone, address, city, country, postal_code):
# ...
Problem: Hard to remember the order, easy to make mistakes.
β Good: Use a Dictionary or Class
def create_user(user_data):
"""Create a user from a dictionary."""
name = user_data["name"]
age = user_data["age"]
email = user_data["email"]
# ...
user_data = {
"name": "Alice",
"age": 25,
"email": "alice@example.com",
"phone": "+1...",
"address": "...",
"city": "New York",
"country": "USA",
"postal_code": "10001"
}
create_user(user_data)
Rule: More than 3-4 parameters β use a dictionary or data structure.
π― Example: Before and After
β Bad Code
def f(x, y):
print("Calculating...")
r = x + y
print("Result:", r)
if r > 100:
print("Big number!")
with open("log.txt", "a") as f:
f.write(f"{r}\n")
return r
result = f(50, 60)
Problems:
- Unclear name f
- Unclear parameters x, y
- Mixing computation and output
- Multiple responsibilities in one function
β Good Code
def calculate_sum(first_number, second_number):
"""
Calculate the sum of two numbers.
Args:
first_number: The first number
second_number: The second number
Returns:
The sum of the two numbers
"""
return first_number + second_number
def is_big_number(number):
"""Check if a number is large (> 100)."""
return number > 100
def log_number(number, filename="log.txt"):
"""Write a number to a file."""
with open(filename, "a") as file:
file.write(f"{number}\n")
def show_result(number):
"""Display the calculation result."""
print(f"Result: {number}")
if is_big_number(number):
print("Big number!")
# Usage
result = calculate_sum(50, 60)
show_result(result)
if is_big_number(result):
log_number(result)
Benefits:
- β
Descriptive names
- β
Each function does one thing
- β
Pure functions (no print in calculations)
- β
Easy to test
- β
Easy to reuse
β‘ Good Function Checklist
β
Name says WHAT it does (verb + object)
β
One function β one job (SRP)
β
Returns a result, doesn’t print (pure function)
β
Descriptive parameter names
β
Doesn’t mutate arguments
β
Has documentation (docstring)
β
Length up to 20-30 lines (fits on screen)
β
Maximum 3-4 parameters
β
Uses default values (where appropriate)
β
Easy to test
π― Example: Banking System
β Bad
def process(acc, amt, t):
print("Processing...")
if t == "d":
acc["b"] += amt
print("Done")
elif t == "w":
if acc["b"] >= amt:
acc["b"] -= amt
print("Done")
else:
print("Error")
print(f"Balance: {acc['b']}")
β Good
def deposit(account, amount):
"""
Deposit funds into an account.
Args:
account: Account dictionary
amount: Amount to deposit
Returns:
Updated balance
"""
account["balance"] += amount
return account["balance"]
def withdraw(account, amount):
"""
Withdraw funds from an account.
Args:
account: Account dictionary
amount: Amount to withdraw
Returns:
Updated balance, or None if insufficient funds
"""
if amount > account["balance"]:
return None
account["balance"] -= amount
return account["balance"]
def show_balance(account):
"""Display the account balance."""
print(f"Balance: {account['balance']}")
# Usage
account = {"name": "Alice", "balance": 1000}
new_balance = deposit(account, 500)
if new_balance:
print(f"Deposited! New balance: {new_balance}")
new_balance = withdraw(account, 200)
if new_balance is not None:
print(f"Withdrawn! New balance: {new_balance}")
else:
print("Insufficient funds!")
show_balance(account)
π Summary
Golden rules for functions:
- Name = action (verb + what it does)
- One job (SRP β Single Responsibility)
- Pure function (return, not print)
- Descriptive parameters (names speak for themselves)
- Don’t mutate arguments (return a new value)
- Document it (docstring is required)
- Keep it short (up to 30 lines)
- Few parameters (up to 4)
- Testable (easy to verify)
- Reusable (can be used in different contexts)
Principle: A function should speak for itself. If you need to read the internals to understand it β the name is bad!
π Related Topics
- Functions Basics π¦
- Clean Code: Comments π¬
- KISS: Keep It Simple π―
- DRY: Don’t Repeat Yourself π
Now your functions speak for themselves! π―β¨
π¬ Comments (0)
No comments yet
Be the first to share your opinion about this article!