Warming up the neural circuits...
By the end of this chapter you will:
Functions are the single most important abstraction in programming. They let you:
calculate_tax(total) is clearer than 10 lines of mathEvery program you'll write from here on uses functions. Master them now, and everything else gets easier.
Functions are reusable blocks of code. Click each pattern to see how to use it:
# Define a function
def greet(name):
print(f"Hello, {name}!")
# Call it
greet("Alice") # Hello, Alice!
greet("Bob") # Hello, Bob!
# Default parameters
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice!
greet("Bob", "Good morning") # Good morning, Bob!| Concept | Syntax |
|---|---|
| Define | def name(params): |
| Call | name(args) |
| Default param | def name(x=10): |
| Return | return value |
Key insight: Functions don't run until you call them. You can call them multiple times.
# WRONG: returns None
def add_bad(a, b):
print(a + b)
result = add_bad(3, 5)
print(result * 2) # TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
# RIGHT: returns value
# Local variable
def my_func():
x = 10 # Local
print(x)
my_func() # 10
print(x) # NameError: name 'x' is not defined
# Global variable
x = 10
def calculate_tax(amount, rate=0.1):
"""
Calculate tax on a given amount.
Args:
amount: The pre-tax amount (float or int)
rate: The tax rate as a decimal (default 0.1 = 10%)
Returns:
The tax amount (float)
Example:
>>> calculate_tax(100, 0.1)
10.0
"""
return amount * rate
# Access docstring
Think of functions as machines:
Define a function with def:
def greet():
print("Hello!")Call it by name:
greet() # Hello!
greet() # Hello! (reusable)Key facts:
Parameters are the names listed in the function definition. Arguments are the values you pass when calling the function.
def greet(name): # 'name' is a parameter
print(f"Hello, {name}!")
greet("Alice") # "Alice" is an argument
greet("Bob") # "Bob" is an argumentMultiple parameters:
def add(a, b):
print(a + b)
add(3, 5) # 8
add(10, 20) # 30Positional vs keyword arguments:
def describe_pet(animal, name):
print(f"My {animal} is named {name}")
# Positional (order matters)
describe_pet("cat", "Whiskers")
# Keyword (order doesn't matter)
describe_pet(
This is the #1 beginner mistake. Let's be crystal clear:
print() outputs text to the terminal. It's for humans to see.
return sends a value back to the caller. It's for the program to use.
# WRONG: This function doesn't return anything
def add_bad(a, b):
print(a + b)
result = add_bad(3, 5)
print(result) # None — because add_bad didn't return anything
# RIGHT: This function returns a value
def
Mental model:
print() is like shouting the answer so everyone in the room hears it.return is like handing the answer to the person who asked.When to use which:
return when the function computes a value that other code needs.print() when the function's purpose is to display something to the user.return, not print.If a function doesn't explicitly return a value, it returns None:
def do_nothing():
pass # 'pass' is a no-op
result = do_nothing()
print(result) # NoneThis is why the add_bad example above returned None — it had no return statement.
When None is useful:
NoneNone when the item isn't founddef find_user(users, name):
for user in users:
if user["name"] == name:
return user
return None # Not foundDocstrings are multi-line comments that document what a function does. They go right after the def line, wrapped in triple quotes:
def calculate_tax(amount, rate=0.1):
"""
Calculate tax on a given amount.
Args:
amount: The pre-tax amount (float or int)
rate: The tax rate as a decimal (default 0.1 = 10%)
Returns:
The tax amount (float)
Example:
>>> calculate_tax(100, 0.1)
10.0
"""
return amount * rateAccess docstrings with help():
help(calculate_tax)
# Help on function calculate_tax in module __main__:
#
# calculate_tax(amount, rate=0.1)
# Calculate tax on a given amount.
# ...When to write docstrings:
When NOT to write docstrings:
def double(x): return x * 2Parameters can have default values, making them optional:
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice!
greet("Alice", "Good morningKey facts:
# RIGHT
def func(a, b, c=10):
pass
# WRONG — SyntaxError
def func(a=10, b, c):
passVariables created inside a function are local — they don't exist outside the function:
def my_func():
x = 10 # Local variable
print(x)
my_func() # 10
print(x) # NameError: name 'x' is not definedVariables created outside functions are global — they're accessible everywhere:
x = 10 # Global variable
def my_func():
print(x) # Can read global variables
my_func() # 10Modifying global variables:
You can read global variables, but to modify them, you need the global keyword:
count = 0
def increment():
global count
count += 1
increment()
print(count) # 1Best practice: Avoid global. Pass values as parameters and return results instead. Global makes code hard to test and debug.
Data processing:
def parse_csv_line(line):
"""Parse a CSV line into a dictionary."""
parts = line.strip().split(",")
return {
"name": parts[0],
"age": int(parts[1]),
"email"
:
def is_valid_email(email):
"""Check if an email has a basic valid format."""
return "@" in email and "." in email.split("@")[-1]
print(is_valid_email("alice@example.com")) # True
printTransformation:
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
temps = [0, 20, 30, 100]
fahrenheit = [celsius_to_fahrenheit(t) for t in temps]
print(| Mistake | Why it's wrong | Fix |
|---|---|---|
Using print() instead of return | The function outputs text but doesn't return a value. Callers get None. | Use return to send values back to the caller. |
| Forgetting to call the function | def greet(): ... defines the function but doesn't run it. | Call it: greet() |
Mutable default argument: def f(items=[]) | The default list is shared across all calls — state leaks. | Use None as the default: def f(items=None): items = items or [] |
Modifying a global variable without global | Python creates a local variable instead of modifying the global one. | Use global keyword, or better: pass as parameter and return. |
| Parameter order mismatch | def f(a, b=10) requires a but is optional. Calling fails. |
Type hints: In production code, annotate parameters and return types:
def calculate_tax(amount: float, rate: float = 0.1) -> float:
return amount * rateType hints are optional and not enforced at runtime, but tools like mypy check them statically. We'll cover type hints in L2.
Pure functions: Prefer functions that don't modify external state (pure functions). They're easier to test, debug, and reason about:
# Pure — no side effects
def add(a, b):
return a + b
# Impure — modifies external state
total = 0
def add_to_total(x):
global total
total += xFunction length: Keep functions short — ideally under 20 lines. If a function is longer, break it into smaller functions. Each function should do one thing.
double that takes a number and returns it multiplied by 2.is_even that returns True if a number is even, False otherwise.greet with name and optional greeting (default "Hello").print() instead of return? Show a buggy example and the fix.find_max that returns the largest number in a list without using max().Beginner:
"What's the difference between a parameter and an argument?"
A parameter is a variable listed in the function definition. An argument is the actual value you pass when calling the function.
def greet(name):—nameis a parameter.greet("Alice")—"Alice"is an argument.
"What's the difference between print() and return?"
print()outputs text to the terminal for humans to see.returnsends a value back to the caller for the program to use. A function that usesprint()instead ofreturnreturns.
def, call them by name.return sends a value back to the caller. print() outputs to the terminal.return return None.help().global to modify globals (but avoid it).I want to…
├── Define a function → def name(params):
├── Call a function → name(args)
├── Return a value → return value
├── Make a parameter optional → def f(x=10):
├── Document a function → """Docstring"""
├── Access docs → help(function_name)
└── Avoid side effects → Use return, not print; avoid globalWhat's the difference between a parameter and an argument?
| Method | Returns | Use when |
|---|---|---|
return value | The value | When caller needs the result |
print(text) | None | When function's purpose is to display |
| No return | None | When function only has side effects |
Warning: Functions without return return None by default.
| Scope | Visibility | Best practice |
|---|---|---|
| Local | Inside function only | Preferred |
| Global | Everywhere | Avoid modifying |
Best practice: Avoid global. Pass values as parameters and return results.
| Purpose | |
|---|---|
| First line | What the function does |
| Args section | Parameter descriptions |
| Returns section | What it returns |
| Example section | Usage example |
When to write: Every function you'll reuse or share.
f(b=5)Always pass required parameters: f(1, b=5) |
Senior:
"What's wrong with def f(items=[])? How do you fix it?"
The default list is created once when the function is defined, not each time it's called. All calls share the same list, so modifications persist across calls. Fix: use
Noneas the default and create a new list inside the function:def f(items=None): items = items or [].
"What's a pure function? Why are they preferred?"
A pure function has no side effects — it doesn't modify external state, and given the same inputs, it always returns the same output. Pure functions are easier to test (no setup/teardown), debug (no hidden state), and reason about (deterministic). They're the foundation of functional programming.