
Quick answer: Define a function with def name(parameters):, indented body, and optional return. Functions stop you repeating yourself, give logic a name you can test, and isolate variables: anything assigned inside a function is local and vanishes when the function ends. Pass data in through parameters, get data out through return — not through global variables.
Part 4 of our Python series. Loops and conditions from the previous article come in handy here.
Why functions matter
Three reasons, in order of career impact: functions let you name an operation (calculate_invoice_total beats reading twenty lines to figure that out); they let you reuse it (write once, call anywhere); and they let you isolate it (a bug inside a function cannot leak variable changes into the rest of your program). Everything from Article 17’s game sprites to Article 19’s threads is built from functions.
Defining and calling
def greet(name): # definition: def, name, params, colon
return f"Hello, {name}!" # body indented 4 spaces
message = greet("Priya") # call: parentheses actually run it
print(message) # Hello, Priya!Defining runs no code — it just registers the function. The call executes the body. Forgetting () is the classic silent bug: greet without parentheses is the function object itself; greet() is its result.
Parameters: positional, keyword, defaults
def order_coffee(size, drink, sugar=0): # sugar has a default
print(f"{size} {drink}, {sugar} sugar")
order_coffee("large", "latte") # positional: order matters
order_coffee(drink="mocha", size="small") # keyword: order does not
order_coffee("small", "tea", 2) # override the defaultRules of thumb:
- Positional first, defaults last —
def f(sugar=0, size)is a SyntaxError. - Keyword arguments are self-documenting; use them beyond two parameters.
- Never use a mutable default (
def f(items=[])— the list persists across calls and accumulates; default toNoneand create inside).
return: getting answers out
def rectangle_stats(w, h):
area = w * h
perimeter = 2 * (w + h)
return area, perimeter # packs a tuple (see Article 2)
a, p = rectangle_stats(3, 4) # unpacks into two names
print(a, p) # 12 14A function with no return statement hands back None. If your code prints None unexpectedly, some path forgot to return.
Scope: local vs global
total = 100 # global — module level
def add_fee(fee):
local_total = total + fee # can READ globals
return local_total
def broken():
total = total + 1 # UnboundLocalError: assignment
# makes total local, read too earlyReading a global from inside a function works. Assigning to it creates a local name instead — unless you declare global total, which you should almost never do. The professional pattern is the inverse: keep functions pure (inputs via parameters, outputs via return) and let the top level of the script own the state.
try/except: functions that survive bad input
def to_int(text): # conversation with user data
try:
return int(text)
except ValueError:
return None
to_int("42") # 42
to_int("four") # None — no crashException handling becomes its own topic later in the series; for now, wrap risky conversions at function boundaries.
Complete executable example
# todo-board.py — functions assembling a real mini program
tasks = [] # state lives at the top
def add_task(title, priority="normal"):
"""Append one task; lower-case the title for consistency."""
tasks.append({"title": title.lower(), "priority": priority})
return len(tasks)
def next_task():
"""Highest priority first. Returns None when empty."""
if not tasks:
return None
order = {"high": 0, "normal": 1, "low": 2}
return min(tasks, key=lambda t: order[t["priority"]])
def show():
for i, t in enumerate(tasks, start=1):
marker = "!" if t["priority"] == "high" else " "
print(f"{i}. [{t['priority']:>6}] {marker} {t['title']}")
add_task("Write article draft", "high") # 1
add_task("Review PRs") # 2
add_task("Archive old backups", "low") # 3
show()
best = next_task()
if best:
print("Do first:", best["title"])Line by line: tasks is global state on purpose so three functions can share it; each docstring is a one-sentence contract; min(..., key=...) ranks by a priority map instead of alphabetically; the if best: guard handles the empty case that next_task() explicitly returns.
Common mistakes and edge cases
NameErroron forward calls —helper()at line 1,def helperat line 10 fails if the call runs before the definition. Define first, call after.- Defining but never calling — a function that is never invoked produces no output and no error except your own confusion. Check for missing
(). - Mutable default arguments — the app scaffolds state across calls. Use
Noneas the default and build inside the function. UnboundLocalError— assignment inside a function makes the name local for the whole function, even lines above the assignment. Rename or pass it in.- Printing when you meant returning —
printshows a value to a human;returnhands it to the calling code. A helper that only prints cannot be composed.
Key takeaways and challenge
defnames an operation; calling with()runs it.- Positional before keyword/defaults; return values, never print-as-output.
- Locals die with the call; globals are readable but almost never writable from a function.
Challenge: extend todo-board.py with remove_task(title) that deletes a matching task and returns True/False, plus a complete_all() that clears the board and returns how many were removed. Then prove next_task() correctly returns None when the board is empty — testing the empty case is a professional habit.
Want one-to-one help getting ramped in Python? Ampersand Academy offers hands-on training.
Do Python functions need to be declared before use?
Yes, at execution time. Python defines functions when the def line runs, so a call placed above the definition fails with NameError unless the call itself sits inside another function.
What is the difference between print and return?
print shows a value to a human and returns None; return hands the value back to the calling code. A helper that only prints cannot be composed into other logic.
What is a mutable default argument and why avoid it?
A default like def f(items=[]) is created once and reused across every call, so appended items accumulate unexpectedly. Default to None and create the list inside the function.
What does UnboundLocalError mean?
You assigned to a variable inside a function, which makes it local for the entire function, then read it before that assignment. Rename the local or pass the value in as a parameter.
How do multiple return values work in Python?
return a, b packs the values into a tuple, and the caller unpacks them: area, perimeter = rectangle_stats(3, 4). It is tuple packing and unpacking, not a special mechanism.
Last updated on · Written by Dinesh Kumar R
