1. def: signature, body, and the single-return rule
A function definition binds a name to a callable object. The `def` keyword names the function, the parentheses list parameters, and the indented block defines the body. Python passes arguments by **object reference**, the same model variables use: the function receives a name pointing to the caller's object. Reassigning the name inside the function does not affect the caller. Mutating the object does.
Every function returns something. Without an explicit `return`, the function returns `None`. This is the cause of the "function prints but returns nothing" confusion: `print(x)` inside a function shows `x` to the user, but the calling code receives `None` from the function. Graders test the **return value**, not the printed output, so a function that only `print`s fails the autograder even when the screen looks right.
The single-return-point rule from C is **not** Python style. Multiple `return` statements are idiomatic and often clearer. Early returns for guard conditions flatten nested `if` blocks: `if not valid: return None` near the top removes the need to indent the rest of the body.
# Run with: python defbasics.py
# Function signature and the print-vs-return trap
def double(n):
"""Return n times 2. Pure function: no side effects."""
return n * 2 # explicit return - graders test THIS value
def double_broken(n):
print(n * 2) # prints to stdout, returns None implicitly
result_good = double(5)
result_bad = double_broken(5) # screen shows 10, but...
print(f"good: {result_good}, bad: {result_bad}") # good: 10, bad: None
# Early-return guard pattern: cleaner than nested if/else
def letter_grade(score):
if not isinstance(score, (int, float)):
return None # guard: bail early on bad input
if score < 0 or score > 100:
return None
if score >= 90: return "A"
if score >= 80: return "B"
if score >= 70: return "C"
if score >= 60: return "D"
return "F"
print(letter_grade(87)) # B
print(letter_grade(-5)) # None
print(letter_grade("ab")) # None