Defining Functions, Signatures, and Return Values
Defining Functions, Signatures, and Return Values
After reading this chapter, you will master the def statement, write clean docstrings, leverage multiple return values, and apply guard clauses to prevent nested logic.
Mental model
In Python, def is an executable statement. When CPython encounters def func(): ... during module execution, it creates a function object (PyFunctionObject) wrapping the compiled bytecode, and binds the function’s name in the current namespace.
Execution of 'def':
Source Code ──▶ Bytecode CodeObject ──▶ PyFunctionObject ──▶ Name binding
- __name__
- __doc__
- __code__
Call Execution:
func() ──▶ Allocates a new PyFrameObject on the call stack
- Holds local namespace
- Executes bytecode
- Destroys frame upon return and yields value to caller
If a function completes without reaching an explicit return statement, Python implicitly returns None.
Minimal example
Save as func_basics.py:
# func_basics.py
def calculate_metrics(values: list[float]) -> tuple[float, float, float]:
"""Compute the minimum, maximum, and average of a non-empty list of values."""
if not values:
raise ValueError("Cannot calculate metrics on empty values list")
total = sum(values)
count = len(values)
return min(values), max(values), total / count
def main() -> None:
latencies = [12.4, 8.9, 15.2, 11.0, 9.7]
min_lat, max_lat, avg_lat = calculate_metrics(latencies)
print(f"Min latency : {min_lat:.1f}ms")
print(f"Max latency : {max_lat:.1f}ms")
print(f"Avg latency : {avg_lat:.1f}ms")
if __name__ == "__main__":
main()Run via uv run python func_basics.py:
Min latency : 8.9ms
Max latency : 15.2ms
Avg latency : 11.4ms
Worked examples
Case 1: Early returns (The “Bouncer Pattern”)
Deeply nested if/else ladders make code hard to read. Use guard clauses with early returns to handle invalid states immediately:
# bouncer_pattern.py
def authenticate_request(token: str | None, is_expired: bool, role: str) -> str:
# Guard 1: Missing token
if not token:
return "DENIED: Missing token"
# Guard 2: Expired token
if is_expired:
return "DENIED: Token expired"
# Guard 3: Insufficient privileges
if role != "admin":
return "DENIED: Requires admin role"
# Main happy path: Unnested and clear
return "AUTHORIZED: Welcome administrator"
if __name__ == "__main__":
print(authenticate_request(None, False, "admin"))
print(authenticate_request("tok_123", True, "admin"))
print(authenticate_request("tok_123", False, "guest"))
print(authenticate_request("tok_123", False, "admin"))Run:
uv run python bouncer_pattern.pyCase 2: Inspecting function introspection attributes
Because functions are first-class objects, you can inspect their metadata:
# func_metadata.py
def transfer_funds(sender: str, recipient: str, amount: float) -> bool:
"""Execute a secure transactional balance transfer."""
return True
def main() -> None:
print(f"Function Name : {transfer_funds.__name__}")
print(f"Docstring : {transfer_funds.__doc__}")
print(f"Code ArgCount : {transfer_funds.__code__.co_argcount}")
print(f"Code Varnames : {transfer_funds.__code__.co_varnames}")
if __name__ == "__main__":
main()Run:
uv run python func_metadata.pyPitfalls
Pitfall 1: Forgetting explicit returns in branches
If one branch of a function returns a value and another branch omits return, the omitted branch returns None:
# Bug:
def get_user_status(active: bool) -> str:
if active:
return "ONLINE"
# Forgot return: implicit return None!Pitfall 2: Mutating input arguments unexpectedly
Avoid modifying mutable arguments (like lists or dictionaries) in place unless that is the explicitly documented purpose of the function. Prefer returning a new object.
Exercises
- Write a function
parse_semver(version: str) -> tuple[int, int, int]that parses"3.14.2"into integer components(3, 14, 2)and handles malformed strings gracefully. - Refactor a deeply nested 3-level
if/elsevalidation function into a clean flat function using guard clauses. - Write a function with a comprehensive docstring following the Google style guide (Args, Returns, Raises). Print
func.__doc__.
Further reading
- Python Language Reference: Section 8.6: Function definitions.
- PEP 257: Docstring Conventions.
- Clean Code Architecture: The Bouncer / Guard Clause Pattern.