Functions and Scope
Write reusable Python functions with clear parameters, safe defaults, predictable returns and a firm grip on local, enclosing and global names
A small shop deploys a returns form on a Friday afternoon. The first customer adds a broken lamp and submits the return. The second customer opens the same form, sees the lamp already in the basket, adds a kettle, and receives a confirmation for both items. No database row links the two customers, so the fault must live in the code.
The suspicious line sits at the top of the module: def add_item(item, cart=[]):. The list in that signature was built once, when Python read the definition, and every call receives the same list. The cart is not per customer, because the function never asked for a fresh one.
A function is a named block of code that runs when another part of the program calls it. The definition writes def, a name, a parameter list and a body; the call supplies arguments and receives a return value. The failure above is not a syntax error, which is why it escaped review and reached a customer.
Key terms
- A named block of code that runs only when another part of the program calls it.
- A name written in a function definition that receives a value when the function is called.
- A value supplied at the call site, matched to a parameter by position or by name.
- The object a function sends back to its caller with a return statement.
- A value written in the signature that is used when the caller omits that argument.
- The region of a program in which a name can be read or changed.
- A nested function that reads a name from the function that encloses it.
- A small anonymous function written as one expression, with no statement body.
- A string literal as the first statement in a function, stored in the function's __doc__ attribute.
- Metadata attached to a parameter or return value for readers and static checking tools.
- A technique where a function calls itself, with a base case that stops the chain.
- A function that depends only on its arguments and changes no outside state.
Function
Parameter
Argument
Return value
Default argument
Scope
Closure
Lambda
Docstring
Annotation
Recursion
Pure function
This lesson is the ninth stop of the course. By the end you will define and call functions with confidence, choose the right parameter form, and follow a name through every scope layer. You will also see why a list default can outlive a call, how annotations help tools, and where recursion stops being a good idea.
1. The Cart That Refused to Forget
A function definition creates a function object and binds it to a name, and the body stays dormant until a call happens. The word def is a statement, not an instruction to run the indented lines.
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("lamp")) # ['lamp']
print(add_item("kettle")) # ['lamp', 'kettle']Reading the definition costs one object and produces no output. The call add_item("lamp") creates a frame, binds "lamp" to item, runs the body and returns the list. The second call reuses the same default list, which is the source of the Friday bug.
A parameter is a name in the definition, and an argument is the value supplied at the call. The parameter item receives the argument "lamp", which is a small distinction with a large vocabulary payoff. The return statement sends a value back to the caller and ends the call at once.
Description
A flowchart for the section 1. The Cart That Refused to Forget, showing the path and decision points described in the surrounding text.
The diagram follows one call from the argument to the returned object. The frame holds the parameter bindings, and the return value travels back to the name that received the call.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A module contains def greet(name): return f"Hello, {name}!" but the module never calls greet. What happens when the module is imported?
2. One Definition, Two Ways to Call
Positional arguments are matched by position, and keyword arguments are matched by name. A call may mix them, provided every positional argument comes before the first keyword argument.
def create_user(name, age, country):
return f"{name}, {age}, from {country}"
print(create_user("Ada", 25, "USA"))
print(create_user(age=30, country="Canada", name="Bob"))The keyword form is longer, and it protects a call from a silent swap when two parameters share a type. create_user(age=30, country="Canada", name="Bob") reads in the order the caller thinks, not the order of the signature. Both calls produce a correctly bound string, because names carry the binding.
Repeating a value for one parameter raises TypeError, and so does an unknown keyword. Python refuses to guess which value the caller meant. The rule is strict on purpose, because a quiet guess would move a wrong value into a report.
The diagram maps the four call forms to the signature slots they fill. Read it from left to right, and note that the star forms are unpacked before binding begins.
Description
A flowchart for the section 2. One Definition, Two Ways to Call, showing the path and decision points described in the surrounding text.
How to answer
Matching exercise with 5 pairs. Match each item on the left with its answer on the right.
Match each call form to the binding it produces.
Drag answers from here:
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A program defines def create_user(name, age, country): ... and then calls create_user(age=30, name="Bob", country="Canada"). Which statement about the call is correct?
3. Defaults and the List That Outlives the Call
A default value is written after an equals sign in the definition, and it is evaluated once when the def statement runs. That timing is the whole story: the default is built at definition, not on every call.
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Ada")) # Hello, Ada!
print(greet("Ada", "Hi")) # Hi, Ada!The string default is safe because strings are immutable. A caller cannot change "Hello" in place, so the shared object never carries state from one call to the next.
The risk appears with a mutable default such as a list or a dictionary. The same object is bound to the parameter on every call, so an append or a key assignment survives after the function returns.
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(add_item("lamp")) # ['lamp']
print(add_item("kettle")) # ['kettle']The safe version uses None as a sentinel and builds a fresh list inside the body. None is immutable, so the default cannot accumulate state, and each caller receives a new list. The pattern works for dictionaries and sets in the same way.
A non-default parameter cannot follow a default parameter in the same section, so def f(a=1, b): raises SyntaxError. Put the required parameters first, and the optional ones after them. The rule keeps the positional match unambiguous.
The sequence diagram shows two calls that share one default list. The list is created during the def step, before either caller arrives.
Description
A sequence diagram for the section 3. Defaults and the List That Outlives the Call, showing the order of messages and replies described in the surrounding text.
A mutable default is shared state hidden inside a signature. Every call that appends to it changes the object that the next call will receive, and no error marks the damage.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A function is defined as def add_item(item, cart=[]): cart.append(item); return cart. A program calls add_item("lamp") and then add_item("kettle"). What does the second call return?
4. Star Args and Double Star Kwargs
A definition that cannot know how many arguments will arrive uses a starred parameter. *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary.
def total(*prices):
return sum(prices)
def record(**fields):
return fields
print(total(3, 4, 5)) # 12
print(record(name="Ada", role="engineer"))
# {'name': 'Ada', 'role': 'engineer'}The names args and kwargs are conventions rather than keywords, so a definition may call them *items and **options. The single and double stars carry the meaning, while the names only help the reader. That freedom matters when a clearer name makes the call site easier to understand.
Order matters in a signature: positional parameters, then *args, then keyword-only parameters, then **kwargs. Python reads the stars as boundaries, so a parameter after **kwargs is impossible.
How to answer
Fill-in-the-blank exercise with 2 blanks. Type the missing words, then check your answer.
Complete the two variadic parameters in the definitions.
The definition def total(blankprices) collects extra positional values into a tuple. The definition def record(blankfields) collects extra keyword values into a dictionary.
Word Bank, drag words into the blanks above:
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
A function defined as def total(*args): return sum(args) collects extra positional arguments into a tuple, and Python requires the parameter to be named args.
5. The Markers That Protect a Signature
A bare * in a signature marks the parameters after it as keyword-only. A slash / marks the parameters before it as positional-only. Both markers are punctuation with a contract, and both appear in modern library code.
def area(width, height, *, unit="m"):
return f"{width * height} square {unit}"
def power(base, exponent, /):
return base ** exponentKeyword-only parameters stop a caller from swapping two values that share a type. area(3, 4, "m") is rejected, while area(3, 4, unit="m") states which value is the unit. The extra keystrokes buy a signature that documents itself at every call site.
Positional-only parameters keep a public name out of the call syntax, which lets a library rename the parameter later. power(base=2, exponent=3) fails, and power(2, 3) succeeds. The marker protects the calling convention while the parameter name stays free to change.
Marker | Position | Effect |
|---|---|---|
/ | after the parameters | those parameters accept no keyword |
* | before the parameters | later parameters accept no position |
*args | after ordinary parameters | collects extra positional values |
**kwargs | last | collects extra keyword values |
Read a signature from left to right: ordinary parameters, then /, then more parameters, then *, then keyword-only parameters, then **kwargs. The markers divide the signature into regions with different calling rules.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A library defines def power(base, exponent, /): return base ** exponent. Which call is accepted?
6. Unpacking at the Call Site
The stars also work in the other direction. *iterable unpacks a sequence into positional arguments, and **mapping unpacks a dictionary into keyword arguments at the call. This is the mirror image of *args and **kwargs in the definition.
def describe(name, role):
return f"{name} is an {role}"
row = ["Ada", "engineer"]
print(describe(*row)) # Ada is an engineer
print(describe(**{"name": "Ada", "role": "engineer"}))Unpacking is useful when the data already lives in a list or a dictionary. A row from a file becomes a call without an index for every field. A duplicate value still raises TypeError, because unpacking does not weaken the binding rules.
How to answer
Ordering exercise with 5 items. Move them into the correct order, then check your answer.
Order the stages of a call that unpacks a list, from first to last.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A program has row = ["Ada", "engineer"] and a function def describe(name, role): return f"{name} is an {role}". Which call passes both list items as positional arguments?
7. Return, None and Several Answers
return does two jobs: it sends a value back and it ends the call immediately. Code after a return in the same branch never runs, which is why an early return can flatten a nest of conditions.
print is not return. Printing writes text to the screen and sends None back, so a caller that assigns the result of a printing function receives None. The screen output and the return value live in different places.
def show_total(price):
print(f"Total: {price}")
def get_total(price):
return price
print(show_total(10)) # prints Total: 10, then prints None
print(get_total(10)) # prints 10Several values come back as one tuple. return quotient, remainder builds (3, 2), and q, r = divide(17, 5) unpacks it into two names. The tuple is a single object, which is why the comma matters.
def divide(a, b):
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = divide(17, 5)
print(q, r) # 3 2A function with no return, or with a bare return, hands back None. That is a valid design for a function that works by side effect, such as one that prints a report. It is a poor design when the caller expects data, because None travels quietly into later code.
If a caller needs a value, write an explicit return on every path. A function that sometimes falls off the end returns None, and that quiet gap is harder to find than a raised error.
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
A function prints a total and contains no return statement. The caller assigns the call result to a name and receives the printed text as a string.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A function ends with return quotient, remainder. A caller writes q, r = divide(17, 5). What are q and r?
8. Scope: The LEGB Ladder
Scope is the region where a name is visible, and Python checks four layers in a fixed order. The order is Local, Enclosing, Global and Built-in, remembered as LEGB. The first layer that holds the name wins, and the search stops there.
message = "global"
def outer():
message = "enclosing"
def inner():
print(message)
inner()
outer() # enclosingThe inner function has no local binding for message, so it checks the enclosing function and finds "enclosing". If the enclosing function had no such name, the search would continue to the module level. A built-in such as len is found only after all three other layers fail.
A nested function that reads a name from its enclosing function is a closure. The name is looked up when the inner function runs, not when it is defined, so the value can change between calls. That late lookup is powerful and easy to misread.
Description
A state diagram for the section 8. Scope: The LEGB Ladder, showing the states and transitions described in the surrounding text.
The state diagram turns the lookup into a path. Each edge names the condition that moves the search to the next layer, and the red state is the only one that raises an error.
9. global, nonlocal and the UnboundLocalError
global name lets a function rebind a module-level name, and nonlocal name lets an inner function rebind a name in the enclosing function. Both are statements, not function calls. Both also make a hidden data path visible, so use them with care.
count = 0
def broken():
count += 1
broken()The call raises UnboundLocalError, because the assignment makes count local for the whole function. The += reads the local name before any value is bound, so the read fails before the increment can happen.
count = 0
def increment():
global count
count += 1
increment()
print(count) # 1The global statement tells Python that count refers to the module-level name. The function can now rebind it, and the caller sees the new value. The function works, although passing the value in and returning the result is usually clearer.
nonlocal does the same job one layer in. An inner function can rebind a name from its enclosing function, which is how a counter can keep state between calls without a global variable.
def make_counter():
value = 0
def step():
nonlocal value
value += 1
return value
return step
counter = make_counter()
print(counter()) # 1
print(counter()) # 2Modifying a mutable global object, such as appending to a global list, does not need global; rebinding the name does. Prefer passing values in and returning results out, because a function that hides its inputs is hard to test.
An assignment anywhere in a function makes that name local for the whole function. A single count += 1 near the bottom can therefore break a read at the top, and the error appears only when the call runs.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A module sets count = 0. A function body contains count += 1 and no global statement. Calling the function raises which error?
10. Functions Are Objects
Functions are first-class objects, so they can be stored in a list, passed to another function and returned from a function. The name is just one reference to the object, and the parentheses are what call it.
def double(x):
return x * 2
def square(x):
return x * x
operations = [double, square]
for operation in operations:
print(operation(3)) # 6, then 9sorted(words, key=len) passes the len function as a value, without calling it. Python calls len once per item during the sort, which is the higher-order pattern in one line. The same shape appears in event handlers, callbacks and test runners.
words = ["tea", "coffee", "juice"]
print(sorted(words, key=len)) # ['tea', 'juice', 'coffee']functools.partial(f, 2) returns a new callable with some arguments already bound. It is the standard way to specialise a function without a lambda. The new callable keeps the original signature shape for the arguments that remain.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, 2)
print(square(3)) # 9Description
A mind map for the section 10. Functions Are Objects, grouping the ideas described in the surrounding text.
The mindmap collects the whole toolkit, and each branch is a question you can now answer. The branches are not separate features, because a signature, a scope and a return value meet in every call.
11. Lambda: One Expression, No Name Required
A lambda is a small anonymous function with one expression and no statement body. lambda x: x + 1 behaves like a one-line function that returns the expression value.
pairs = [("tea", 3), ("coffee", 6), ("juice", 5)]
print(sorted(pairs, key=lambda pair: pair[1]))
# [('tea', 3), ('juice', 5), ('coffee', 6)]A lambda is useful as a key= argument and poor for anything that needs a name, a docstring or more than one step. When the expression needs a conditional or a second line, write a named function instead.
def by_second(pair):
return pair[1]
print(sorted(pairs, key=by_second))The named version is easier to test and easier to reuse, and it gives the reader a word for the rule. The lambda remains a compact tool for a rule used once, in one call.
Use a lambda when the rule fits on one line and has no name worth remembering. Promote it to a def when a second caller appears or when the expression needs a comment.
12. Docstrings and Annotations: Notes for People and Tools
A docstring is a string literal as the first statement in a function. It becomes function.__doc__ and is read by help(). PEP 257 asks for triple quotes and a one-line summary, with a blank line before any longer explanation.
def calculate_bmi(weight: float, height: float) -> float:
"""Return the body mass index for a weight in kilograms and a height in metres."""
return round(weight / height ** 2, 1)
print(calculate_bmi(70, 1.75)) # 22.9
print(help(calculate_bmi))An annotation such as def f(x: int) -> str: attaches metadata to a parameter and to the return value. Python does not enforce annotations at run time; tools such as mypy and pyright read them. A caller can still pass a string to calculate_bmi, and the division will fail only when the arithmetic runs.
Built-in generic annotations such as list[str] and dict[str, int] work from Python 3.9. From Python 3.14, annotations are evaluated lazily under PEP 649, so a forward reference no longer needs from __future__ import annotations. The syntax and the beginner rules in this lesson are unchanged in 3.15.
PEP 8 asks for lowercase function names with underscores between words, because a reader should not have to decode capital letters. The same guide asks for two blank lines around top-level definitions, which keeps the eye from merging one function into the next.
Dropbox type checked close to four million lines of Python and reported the work on its engineering blog. Annotations are the metadata that made a check at that scale possible, because a tool can read them without running the code.
13. Recursion, Base Cases and the 1000-Call Ceiling
Recursion is a function that calls itself, and every recursive function needs a base case that returns without another call. Without a base case, the calls stack up until the interpreter stops the program with RecursionError.
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120The call factorial(5) returns 120, and the base case factorial(0) returns 1. Each pending call waits for the next result, then multiplies on the way back, so the answer is assembled from the bottom up.
CPython's default recursion limit is 1000 calls, and sys.setrecursionlimit changes it at your own risk. A deeper limit can exhaust the C stack, so iteration is the safer choice for long chains.
fib(10) returns 55 with base cases fib(0) == 0 and fib(1) == 1, though the naive version repeats work and grows quickly. Memoisation belongs to a later lesson; for now, notice that the base cases carry the whole definition.
A recursive function is a loop written as a call. The base case is the loop condition, and the recursive call is the step. If you cannot name the base case in one sentence, rewrite the code as a loop.
14. Pure Functions and Side Effects
A pure function depends only on its arguments and returns a value without changing outside state. A side effect is anything else the call does, such as printing, writing a file or changing a global list.
Pure functions are easier to test, because the same arguments always produce the same result. A function that mixes a side effect with a meaningful return value is harder to test and harder to reuse.
Property | Pure function | Side-effecting function |
|---|---|---|
Inputs | arguments only | arguments plus outside state |
Output | one return value | a change, a print or a file write |
Repeatable | same result every time | result may depend on history |
Testing | compare the return value | inspect the outside world |
Example | round(2.5) | print("done") |
Prefer a pure core and a thin shell. The pure part computes the answer, and the shell prints it or writes it, so each part has one job. That split also keeps the interesting logic away from the parts that are hard to test.
15. Functions in the Python 3.15 World
2026 Updates (as of November 2026)
What changed recently, and why older tutorials may look different. Every line below was checked against a primary source in November 2026:
- Python 3.15.2 is the current stable release (3.15.0 shipped on 2026-10-09, and 3.15.2 followed on 2026-10-25). The function syntax in this lesson is unchanged in 3.15, so every example runs as written.
- Python 3.10 reached end of life on 2026-10-01. The positional-only marker from PEP 570 arrived in 3.8, so it is available on every supported series today.
- Annotations are deferred in 3.14 and later. PEP 649 evaluates them lazily, so a forward reference no longer needs a future import. They remain metadata for tools, not a run-time type check.
The language rules for defaults, scope and returns are settled, so new code can rely on them without a version check. The annotations story is the part that changed most recently, and it is also the part most likely to appear in older blog posts.
Real-World Case Studies
Dropbox: type checking at scale. Dropbox reported type checking close to four million lines of Python on its engineering blog. Those checks depend on annotations attached to function parameters and return values, which is why a small piece of metadata matters at company scale.
The standard library: functions as arguments. The built-in sorted accepts a key function and calls it once per item, and functools.partial returns a new callable with arguments already bound. Both patterns treat a function as data, which is the first-class idea from section 10 in everyday code.
Request handlers. A web framework registers a function as a handler and calls it when a request arrives. The request fields become arguments, the return value becomes the response body, and the framework owns the loop. A handler with a mutable default would leak state across requests, which is the Friday bug at server scale.
📚 Did you know? A function object can be stored in a list and called later, because the parentheses are an operation on the object and not part of its name. That is how callbacks, event handlers and test runners all work.
Practice Questions
Answer these before the next lesson; the final two questions reach back to earlier lessons. Each stem stands alone, so you can answer it without the surrounding prose.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A program runs scores = {"ada": 92}, then value = scores.get("grace", 0), then scores["grace"] = value + 1. What does scores hold afterward?
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
A program runs row = [1, 2], then grid = [row] * 2, then grid[0].append(3). Both rows in grid then contain [1, 2, 3].
What Comes Next
You can now define a reusable block, choose a parameter form that fits the caller, and follow a name through every scope layer. The next lesson moves up one level, because a module is where a group of functions becomes a package that other programs can import.
Before moving on, the video below is a full beginner course that replays this material in a second voice. Its chapters on functions and scope are a good parallel reference when you want the same ideas from another teacher.
Description
A video walkthrough that supports the section What Comes Next and repeats its main ideas in a second voice.
Dig deeper
- Defining functions, defaults, keyword arguments and unpackingPython 3.15 documentation
- The grammar for function definitions and parameter markersPython 3.15 documentation
- Built-in functions such as sorted, len and helpPython 3.15 documentation
- functools.partial and other higher-order helpersPython 3.15 documentation
- PEP 257, Docstring ConventionsPEP index
- PEP 484, Type HintsPEP index
Key Takeaways:
- A
defstatement creates a function object, and the body runs only when a call supplies arguments. - Positional arguments bind by position, keyword arguments bind by name, and the two forms can mix in one call.
- Defaults are evaluated once at definition time, so a mutable default is shared state; use
Noneand build a fresh container inside the body. *argscollects extra positional values into a tuple, and**kwargscollects extra keyword values into a dictionary.- A bare
*makes later parameters keyword-only, and a/makes earlier parameters positional-only. returnsends a value back and ends the call,printonly writes to the screen, and a function withoutreturnreturnsNone.- Returning several values builds a tuple, which the caller can unpack into separate names.
- Name lookup follows LEGB: Local, Enclosing, Global, Built-in, and an assignment makes a name local unless
globalornonlocalsays otherwise. - Functions are objects, so they can be stored, passed and returned; a lambda is a one-expression tool, not a replacement for every function.
- Docstrings and annotations document a function for people and tools, while recursion needs a base case and respects the 1000-call limit.
- Pure functions are easier to test than functions that hide state, so keep the computation pure and the side effects thin.
References
Every factual claim links to its source, with the access date printed so the source can be rechecked.