Unit 1 · Fundamentals

Lesson · Unit 1 · 9 min read

Functions, how you stop repeating yourself.

A function is a named, reusable piece of code. Once you can write one, you stop copy-pasting the same five lines around your file. Here's the syntax, the parameter rules, and the scoping behavior that catches every beginner.

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 ARGUMENTS

Parameters 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 30

One 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 cart

Section · 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 later

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

The 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 + 1

Reaching 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)    # 2

The 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.

Curriculum source

Lesson content is original to YorkSims. Topic structure aligns with Python for Everybody by Dr. Charles R. Severance (py4e.com), licensed under Creative Commons Attribution 3.0 Unported.