Lesson 12: Functions — Reusable Building Blocks
Define functions, parameters, default arguments, return values, and docstrings.
Why Functions?
A function is a named block of reusable code. Write it once, call it many times. Functions are how you stop repeating yourself (the DRY principle: Don't Repeat Yourself) and keep programs readable. If you find yourself copying and pasting the same 5 lines, that code belongs in a function.
Functions are the single most important organizational tool in programming. Every real program — from a web server to a data pipeline — is a collection of functions working together. This lesson gives you the complete mental model: how to write them, call them, and reason about them.
What You'll Learn in This Lesson
- Define functions with
def - Pass parameters and use default arguments
- Return values — and understand
None - Write docstrings that document your code
- Understand local vs global scope
Function Anatomy
def greet(name, excited=True):
"""Return a friendly greeting."""
message = f"Hello, {name}!"
if excited:
message += " 🎉"
return message
| Part | Purpose |
|---|---|
def |
Keyword that defines a function |
greet |
Function name (snake_case) |
name, excited=True |
Parameters (inputs) |
: |
Ends the header — body must be indented |
"""...""" |
Docstring — documentation |
return message |
Hands a value back to the caller |
Call it like this:
print(greet("Amol")) # Hello, Amol! 🎉 (uses default)
print(greet("Riya", False)) # Hello, Riya!
print(greet(name="Sam")) # keyword argument — self-documenting
Mental model: a function is a machine. Inputs go in the funnel (parameters), work happens inside, and the result rolls out (return). Trace each call separately — a fresh set of local boxes every time.
Parameters vs Arguments
- Parameters are the names in the function definition.
- Arguments are the values you pass when calling.
| Calling style | Example | Notes |
|---|---|---|
| Positional | area(5, 4) |
Order matters |
| Keyword | area(length=5, width=4) |
Order-free, self-documenting |
| Default | discount(200) |
Omits optional params |
def area(length, width):
return length * width
area(5, 4) # positional
area(width=4, length=5) # keyword — order doesn't matter
Which style should you use? Positional for short, obvious calls (area(5, 4)); keyword for clarity when a call has several arguments or the meaning isn't obvious. Professionals mix both: send_email("riya@example.com", subject="Hello", urgent=True).
Default Arguments
Parameters can have default values, making them optional:
def discount(price, percent=10):
return price * (1 - percent / 100)
print(discount(200)) # 180.0 (uses 10%)
print(discount(200, 50)) # 100.0 (overrides to 50%)
Rules:
- Defaults come after required parameters:
def f(a, b=1):✅,def f(a=1, b):❌ - Defaults are evaluated once at definition time — never use a mutable default like
def f(items=[])(see the mistakes section).
The mutable-default trap in detail:
def add_item(item, cart=[]): # BAD — cart is shared across calls!
cart.append(item)
return cart
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] — the first call's list leaked in!
The default [] is created once when the function is defined, then reused on every call. The fix is the None sentinel pattern:
def add_item(item, cart=None): # GOOD
if cart is None:
cart = []
cart.append(item)
return cart
return — and the Mystery of None
return hands a value back to the caller and exits the function immediately:
def shout(text):
print(text.upper()) # prints, but returns NOTHING
result = shout("hello") # HELLO
print(result) # None
Every function returns something. If you don't write return, Python returns None automatically. None is a special value meaning "nothing here".
| Behavior | Returns |
|---|---|
Has return value |
That value |
Has bare return |
None |
| No return statement | None |
print() vs return — the eternal beginner question:
print()shows a value on the screen;returnhands a value to the caller. A function that only prints cannot be used in further calculations. A function that returns can:double = area(5, 4) * 2. When in doubt, return — you can always print at the call site.
Docstrings — Documentation Built In
A triple-quoted string right after the def line documents what the function does:
def calculate_grade(score, bonus=0):
"""Return a letter grade for a score plus optional bonus."""
...
Professional teams treat docstrings as non-negotiable. Tools like help(), IDEs, and documentation generators read them automatically.
Writing a good docstring — the three W's:
- What it does (one sentence, imperative: "Return...", "Compute...", "Validate...").
- What it takes (parameters) and returns, when it isn't obvious.
- What can go wrong (exceptions raised).
def withdraw(balance, amount):
"""Subtract amount from balance.
Args:
balance: Current account balance (float).
amount: Amount to withdraw (float).
Returns:
New balance after withdrawal.
Raises:
ValueError: If amount exceeds balance.
"""
Scope — Where Names Live
Variables created inside a function stay inside it (local scope):
language = "Python" # global scope
def show():
print(language) # can READ globals ✅
greeting = "Hi" # local to show() — dies when show() ends
- A function can read global variables.
- It cannot change them without the
globalkeyword (Lesson 14). - Each call creates a fresh set of local boxes — no leftovers between calls.
Why scope is a feature, not a limitation: because functions can't accidentally overwrite your global variables, large programs stay safe. Each function is an isolated universe — that isolation is what makes code predictable and testable.
Common Mistakes to Avoid
- Mistake:
def f(items=[])— mutable default shared across calls — Fix: usedef f(items=None): items = [] if items is None else items. - Mistake: Forgetting
returnand gettingNoneback — Fix: remember: no return = None. - Mistake: Calling a function before defining it in the file — Fix: definitions run top-to-bottom; define before you call (or put calls in a
main()). - Mistake: Using
print()when you need the value for further math — Fix:returnthe value. - Mistake: Naming a function with an uppercase letter or spaces — Fix: snake_case:
calculate_grade, notCalculateGrade.
Professional Tips & Tricks
- One function = one job. If a function does three things, split it.
- Use keyword arguments at call sites:
calculate_grade(score=88, bonus=5). - Write the docstring FIRST — it forces you to think about what the function should do.
- Prefer
returnoverprintinside functions — returned values are reusable. - Keep functions short (roughly under 20 lines); long functions are a signal to split.
Key Takeaways
def name(params):defines a function; the body must be indented.- Defaults make parameters optional; defaults go last.
- Every function returns a value — explicit
returnorNone. - Docstrings document functions and are read by
help()and IDEs. - Local scope keeps function variables isolated.
- Never use mutable defaults; use the
Nonesentinel pattern.
Next up: Lambda, *args & **kwargs — flexible and anonymous functions.
# Defining and calling functions
def calculate_grade(score, bonus=0):
"""Return a letter grade for a score plus optional bonus."""
total = score + bonus
if total >= 90:
return "A"
if total >= 75:
return "B"
if total >= 60:
return "C"
return "F"
# Call with default bonus
print("Score 88:", calculate_grade(88))
# Call with explicit bonus
print("Score 88 + 5:", calculate_grade(88, 5))
# Named arguments make calls self-documenting
print("Named:", calculate_grade(score=72, bonus=3))
# Functions without return give None
def shout(text):
print(text.upper())
result = shout("hello")
print("Return value:", result)Lesson Code (Python)
# Defining and calling functions
def calculate_grade(score, bonus=0):
"""Return a letter grade for a score plus optional bonus."""
total = score + bonus
if total >= 90:
return "A"
if total >= 75:
return "B"
if total >= 60:
return "C"
return "F"
# Call with default bonus
print("Score 88:", calculate_grade(88))
# Call with explicit bonus
print("Score 88 + 5:", calculate_grade(88, 5))
# Named arguments make calls self-documenting
print("Named:", calculate_grade(score=72, bonus=3))
# Functions without return give None
def shout(text):
print(text.upper())
result = shout("hello")
print("Return value:", result)Console Output
Score 88: B
Score 88 + 5: A
Named: C
HELLO
Return value: NoneCode Visualization Tips
- Trace each function call separately — parameters and locals live in their own box.
- Draw the 'return' as the machine dropping the result into the caller's hands.
- Use print() inside the function while learning to watch the machine work.
Professional Tips & Tricks
- One function = one job. If a function does three things, split it.
- Use keyword arguments at call sites: calculate_grade(score=88, bonus=5).
- Write the docstring FIRST — it forces you to think about what the function should do.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: Keyword-Only Bill Calculator 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 12: Python Functions: Parameters, Returns & Type Hints
Up next · Continue learning
Lambda, *args & **kwargs
Anonymous one-line functions and flexible functions that accept any number of arguments.