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

2. 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.3

3. 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.4

4. 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) 7 and 30 on separate lines
  • B) 7 only
  • C) 30 only
  • 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.4

5. 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.3

6. 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.4

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

8. 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.3

9. 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.4

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

Section 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.1

12. "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.2

13. "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.1

14. "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.4

Section 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 debugging

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