Site icon Ampersand Tutorials

Python Functions: def, Parameters, Return Values, and Scope

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 default

Rules of thumb:

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 14

A 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 early

Reading 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 crash

Exception 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

Key takeaways and challenge

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.

Exit mobile version