Section · 01
Defining and calling
You define a function with def, give it a name, list its parameters in parentheses, and indent the body:
def greet(name):
print(f"Hi, {name}.")Defining doesn’t run anything. To run it, you call the function with parentheses:
greet("Ada") # Hi, Ada.
greet("Lovelace") # Hi, Lovelace.Three reasons to write a function:
1. The same code shows up in more than one place.
2. A chunk of code has a clear, nameable job.
3. You want to test it in isolation.Don’t pre-emptively wrap every line in a function. Extract one when you find yourself copying code or when a block deserves a name.
Section · 02
Parameters and arguments
People mix these terms up constantly:
def discount_price(price, percent): # 'price' and 'percent' are PARAMETERS
return price - (price * percent)
final = discount_price(49.99, 0.20) # 49.99 and 0.20 are ARGUMENTSParameters are the placeholder names in the definition. Argumentsare the actual values you pass when calling. You’ll see both words used loosely, but knowing the distinction makes error messages easier to read.
Positional vs keyword arguments
def book(name, room, hour):
print(f"Booking {name} into room {room} at {hour}.")
# Positional — order matters
book("Ada", 3, 14)
# Keyword — order doesn't matter, names do
book(hour=14, name="Ada", room=3)
# Mixed — positional first, then keyword
book("Ada", room=3, hour=14)For functions with 4+ arguments, keyword arguments are gold: they document themselves at the call site. Future-you reading book("Ada", 3, 14) in 6 months will have no idea what 3 and 14 mean.
Default values
def fetch(url, timeout=30, retries=3):
...
fetch("https://...") # timeout=30, retries=3
fetch("https://...", timeout=60) # retries still 3
fetch("https://...", retries=5) # timeout still 30One trap: never use a mutable default like [] or {}. They’re created once when the function is defined, and reused across every call:
# WRONG — the list is shared across all calls
def add_item(item, cart=[]):
cart.append(item)
return cart
add_item("apple") # ["apple"]
add_item("bread") # ["apple", "bread"] — surprise!
# RIGHT — create a new list per call
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cartSection · 03
Return values
A function does one of two things: it has a side effect (prints, writes a file, sends an email), or it returns a value. Most useful functions return.
def total_with_tax(subtotal, rate):
return subtotal + (subtotal * rate)
# The return value can be used like any other expression:
checkout_total = total_with_tax(100, 0.08) # 108.0
print(total_with_tax(50, 0.08)) # 54.0
if total_with_tax(99, 0.08) > 100:
print("Pricey.")A function without a return statement returns None automatically. This trips people up when they mean to print and forget to return:
def double(x):
print(x * 2) # this PRINTS but doesn't RETURN
result = double(5) # prints "10"
print(result) # None — there's nothing to use laterA function can return multiple values by separating them with commas. Python packs them into a tuple; you can unpack at the call site:
def split_name(full):
parts = full.split(" ", 1)
return parts[0], parts[1] # returns a tuple
first, last = split_name("Ada Lovelace")
print(first) # "Ada"
print(last) # "Lovelace"Section · 04
Local scope — the rule everyone misses
Variables you create inside a function only exist inside that function. They’re destroyed when the function returns. This is called local scope.
def compute():
result = 42
return result
compute()
print(result) # NameError — 'result' doesn't exist out hereThe opposite is also true: assigning to a name inside a function creates a new local, even if there’s a variable with the same name outside.
total = 0
def add_one():
total = total + 1 # UnboundLocalError — total is local but never set
# If you actually want to modify the outer total, declare it global:
def add_one():
global total
total = total + 1Reaching for global is almost always a sign you should refactor: return a value and let the caller decide what to do with it.
# Cleaner — no globals, returns a new value
def add_one(total):
return total + 1
total = 0
total = add_one(total)
total = add_one(total)
print(total) # 2The principle: a function takes inputs, returns outputs, and minds its own business. The fewer outside things it reaches for, the easier it is to test, reuse, and read six months later.
Section · 05
A real refactor
Here’s a script with duplication. Watch it shrink:
# BEFORE: same calculation repeated three times
laptop_price = 1200
laptop_tax = laptop_price * 0.0725
laptop_total = laptop_price + laptop_tax
phone_price = 800
phone_tax = phone_price * 0.0725
phone_total = phone_price + phone_tax
tablet_price = 600
tablet_tax = tablet_price * 0.0725
tablet_total = tablet_price + tablet_tax# AFTER: one function, three calls
def with_tax(price, rate=0.0725):
return price + (price * rate)
laptop_total = with_tax(1200)
phone_total = with_tax(800)
tablet_total = with_tax(600)One change to the tax rate? Edit one line instead of three. New item? One line, not three. Test the function once and you know all three calls work. That’s what functions are for.