Quiz: Functions — Writing Reusable Code
Test your understanding before moving on. Target: 70% or higher to proceed confidently.
Section 1: Multiple Choice (1 point each)
1. What keyword is used to define a function in Python?
- A)
function - B)
def - C)
fn - D)
define
Answer
**B)** `def` *Why B:* Python uses `def` (short for "define") to create functions. *Why not A:* `function` is used in JavaScript, not Python. *Why not C/D:* Neither `fn` nor `define` are Python keywords. *Reference:* Section 6.22. What is the difference between a parameter and an argument?
- A) They are the same thing — different words for the same concept
- B) A parameter is the variable in the function definition; an argument is the value passed when calling
- C) A parameter is for input; an argument is for output
- D) Parameters are required; arguments are optional
Answer
**B)** A parameter is the variable in the function definition; an argument is the value passed when calling *Why B:* In `def greet(name):`, `name` is a parameter. In `greet("Alice")`, `"Alice"` is an argument. *Why not A:* They are related but distinct concepts. *Why not C:* Both relate to input; `return` handles output. *Why not D:* Parameters can have defaults (making their arguments optional), but this isn't the core distinction. *Reference:* Section 6.33. What does this function return?
def mystery(x):
x = x * 2
print(x)
- A) The doubled value of x
- B)
None - C) The original value of x
- D) An error
Answer
**B)** `None` *Why B:* The function has no `return` statement, so it implicitly returns `None`. It prints the doubled value, but printing is not the same as returning. *Why not A:* The doubled value is printed but not returned. *Why not C:* Nothing is returned — the original value isn't special. *Why not D:* The code is valid; it just doesn't explicitly return a value. *Reference:* Section 6.44. What is the output of this code?
def add(a, b):
return a + b
result = add(3, 4)
print(result)
print(add(10, 20))
- A)
7and30on separate lines - B)
7only - C)
30only - D) Nothing — functions only compute, they don't display
Answer
**A)** `7` and `30` on separate lines *Why A:* `add(3, 4)` returns `7`, which is stored in `result` and printed. `add(10, 20)` returns `30`, which is printed directly. *Why not B/C:* Both `print` calls execute. *Why not D:* The `print()` calls are outside the function — they definitely display output. *Reference:* Section 6.45. Which of the following is a valid function definition with a default parameter?
- A)
def greet(greeting="Hello", name): - B)
def greet(name, greeting="Hello"): - C)
def greet(name="Alice", greeting="Hello"): - D) Both B and C
Answer
**D)** Both B and C *Why D:* Parameters with defaults must come after parameters without defaults. In B, `name` (no default) comes before `greeting` (has default) — valid. In C, both have defaults — also valid. *Why not A:* `greeting` has a default but comes before `name` (no default), which is a `SyntaxError`. *Reference:* Section 6.3.36. What does *args do in a function definition?
- A) Makes all arguments required
- B) Collects extra positional arguments into a tuple
- C) Collects extra keyword arguments into a dictionary
- D) Multiplies the arguments together
Answer
**B)** Collects extra positional arguments into a tuple *Why B:* The `*` in `*args` packs all extra positional arguments into a tuple named `args`. *Why not A:* `*args` makes the function accept *any number* of arguments — the opposite of requiring specific ones. *Why not C:* That's `**kwargs` (covered in a later chapter). *Why not D:* The `*` is a packing operator, not multiplication. *Reference:* Section 6.3.47. What is the output?
x = 5
def change():
x = 10
change()
print(x)
- A)
5 - B)
10 - C)
None - D)
NameError
Answer
**A)** `5` *Why A:* The `x = 10` inside `change()` creates a *local* variable `x` that shadows the global `x`. The global `x` is never modified. *Why not B:* The function modifies only its local `x`, not the global one. *Why not C:* The `print(x)` accesses the global `x`, which is `5`. *Why not D:* The global `x` exists and is accessible. *Reference:* Section 6.58. Which error will this code produce?
total = 100
def update_total():
total = total + 50
return total
update_total()
- A)
SyntaxError - B)
TypeError - C)
UnboundLocalError - D) No error — it works fine
Answer
**C)** `UnboundLocalError` *Why C:* The assignment `total = total + 50` makes Python treat `total` as a local variable. But `total + 50` tries to read the local `total` before it has been assigned — hence `UnboundLocalError`. *Why not A:* The syntax is valid. *Why not B:* The types are compatible (int + int). *Why not D:* The shadowing issue causes a runtime error. *Reference:* Section 6.5.39. What is the LEGB rule?
- A) A coding style guideline
- B) Python's variable lookup order: Local, Enclosing, Global, Built-in
- C) A function naming convention
- D) The order in which Python executes function calls
Answer
**B)** Python's variable lookup order: Local, Enclosing, Global, Built-in *Why B:* When Python encounters a variable name, it searches in this order: Local scope, Enclosing function scopes, Global scope, Built-in names. *Why not A:* LEGB is about variable resolution, not coding style. *Why not C:* It has nothing to do with naming. *Why not D:* Function execution order is tracked by the call stack, not LEGB. *Reference:* Section 6.5.410. What is the purpose of a docstring?
- A) To make the function run faster
- B) To document what a function does, accessible via
help() - C) To test the function automatically
- D) To prevent the function from being modified
Answer
**B)** To document what a function does, accessible via `help()` *Why B:* A docstring is a string literal that describes the function's purpose, parameters, and return value. Python makes it available through `help()` and `__doc__`. *Why not A:* Docstrings have no effect on performance. *Why not C:* While docstrings *can* contain doctests, that's not their primary purpose. *Why not D:* Docstrings don't provide any protection against modification. *Reference:* Section 6.7Section 2: True/False with Justification (1 point each)
11. "A function must always have at least one parameter."
Answer
**False** *Explanation:* Functions can have zero parameters. For example, `def display_menu():` takes no parameters — it performs the same action every time it's called. Functions that always do the same thing (like printing a menu) don't need input data. *Reference:* Section 6.2.112. "Using print() inside a function is the same as using return."
Answer
**False** *Explanation:* `print()` displays a value to the screen but the value is lost to the program — it can't be stored in a variable or used in further calculations. `return` sends a value back to the caller, where it can be stored, passed to other functions, or used in expressions. *Reference:* Section 6.4.213. "If you define a variable inside a function, it can be accessed outside the function."
Answer
**False** *Explanation:* Variables defined inside a function are local to that function. They exist only while the function is executing and are destroyed when the function returns. Trying to access them outside the function raises a `NameError`. *Reference:* Section 6.5.114. "A function that doesn't have a return statement returns None."
Answer
**True** *Explanation:* If a function has no `return` statement, or reaches the end without executing a `return`, Python implicitly returns `None`. This is by design — every function call in Python produces a value. *Reference:* Section 6.4.4Section 3: Short Answer (2 points each)
15. Explain why this code is dangerous and show how to fix it:
def add_student(name, roster=[]):
roster.append(name)
return roster
Sample Answer
The default list `[]` is created once when the function is defined and shared across all calls. Each call modifies the same list, so students accumulate across calls instead of each call starting with a fresh list. **Fix:**def add_student(name, roster=None):
if roster is None:
roster = []
roster.append(name)
return roster
Using `None` as the default and creating a new list inside the function ensures each call starts fresh (unless a roster is explicitly provided).
*Rubric — full credit requires:*
- Identifying the mutable default argument problem
- Explaining that the list is shared across calls
- Providing the `None`-based fix
16. Consider this call stack snapshot:
| validate_input(user_input="abc") | <-- top
| process_form(data={...}) |
| handle_request(request=<Request>) |
| main() |
|______________________________________|
Which function is currently executing? If validate_input raises an error, in what order will Python report the functions in the traceback? Why is this order useful for debugging?
Sample Answer
`validate_input` is currently executing (it's on top of the stack). Python reports tracebacks from bottom to top: `main()` -> `handle_request()` -> `process_form()` -> `validate_input()`. The actual error appears at the bottom of the traceback output (from `validate_input`). This order is useful because it shows the entire chain of calls that led to the error — you can trace exactly how the program arrived at the failing function. Reading from the bottom, you see the error itself first, then work upward to understand the calling context. *Rubric — full credit requires:* - Correctly identifying `validate_input` as currently executing - Correct traceback order (bottom to top in terms of call chain) - Explanation of why the chain is useful for debugging17. Why is this considered bad function design, and how would you improve it?
def process(d, n, t):
r = d * n
tax = r * t
return r + tax
Sample Answer
Problems: 1. **Vague function name:** `process` doesn't describe what it does. 2. **Cryptic parameter names:** `d`, `n`, `t`, `r` tell you nothing. 3. **No docstring:** No documentation explaining inputs, outputs, or purpose. Improved version:def calculate_total_with_tax(price, quantity, tax_rate):
"""Calculate the total cost including tax.
Parameters:
price (float): Unit price of the item.
quantity (int): Number of items.
tax_rate (float): Tax rate as a decimal (e.g., 0.08 for 8%).
Returns:
float: Total cost including tax.
"""
subtotal = price * quantity
tax = subtotal * tax_rate
return subtotal + tax
*Rubric — full credit requires:*
- Identifying at least two of the three problems (naming, parameters, documentation)
- Providing a renamed version with descriptive names
- Including a docstring
Section 4: Code Analysis and Tracing (3 points each)
18. Trace through this code and write the exact output. Show your work by tracking variable values at each step:
def double(n):
return n * 2
def add_one(n):
return n + 1
def transform(value):
step1 = double(value)
step2 = add_one(step1)
return step2
result = transform(5)
print(result)
print(double(add_one(3)))
Answer
11
8
**Trace for `transform(5)`:**
1. `transform(5)`: `value = 5`
2. `step1 = double(5)` -> `5 * 2 = 10`, so `step1 = 10`
3. `step2 = add_one(10)` -> `10 + 1 = 11`, so `step2 = 11`
4. Returns `11`, stored in `result`
5. `print(result)` -> prints `11`
**Trace for `double(add_one(3))`:**
1. Inner call first: `add_one(3)` -> `3 + 1 = 4`
2. Outer call: `double(4)` -> `4 * 2 = 8`
3. `print(8)` -> prints `8`
19. Predict the output of this code and explain why:
def outer():
result = []
def add_greeting(name):
result.append(f"Hello, {name}!")
add_greeting("Alice")
add_greeting("Bob")
return result
greetings = outer()
print(greetings)
print(len(greetings))
Answer
['Hello, Alice!', 'Hello, Bob!']
2
**Explanation:** `add_greeting` is a nested function defined inside `outer`. It can access `result` from the enclosing scope (the "E" in LEGB). `result.append()` modifies the existing list object — it doesn't create a new local variable, so no `UnboundLocalError` occurs. After two calls to `add_greeting`, `result` contains two strings. `outer()` returns this list.
20. What is wrong with this function, and what will happen when it is called?
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
print(f"{n}! = {result}")
answer = factorial(5)
next_step = answer + 1
Answer
The function prints `"5! = 120"` but does not return anything — it implicitly returns `None`. The variable `answer` is therefore `None`. The line `next_step = answer + 1` raises a `TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'`. **Fix:** Replace `print(f"{n}! = {result}")` with `return result` (or add `return result` after the print statement if you want both behaviors). *Rubric — full credit requires:* - Identifying that `print` is used instead of `return` - Predicting the `TypeError` on the `answer + 1` line - Explaining that `answer` is `None`Scoring & Next Steps
| Score | Assessment | Recommended Action |
|---|---|---|
| < 50% | Needs review | Re-read sections 6.2-6.5, redo Part A exercises |
| 50-70% | Partial | Review weak areas, especially scope and return values |
| 70-85% | Solid | Ready to proceed; revisit any missed topics |
| > 85% | Strong | Proceed to Chapter 7; consider extension exercises |