A function is a block of code that performs a specific task and can be reused anywhere in your program. Instead of writing the same code 10 times, you write it once in a function and call it 10 times.
From your file (functions.py), four key reasons:
| Reason | Meaning |
|---|---|
| Code Reusability | Write once, use many times |
| Modularity | Break big programs into smaller, manageable pieces |
| Maintainability | Fix a bug in one place, it's fixed everywhere |
| Abstraction | Hide complex logic behind a simple function name |
Python has three types:
print(), input(), type(), len()defdef function_name(parameters): # code block return value
From your file — two examples:
# Function with no parameters def add_two_numbers(): a = 10 b = 10 sum = a + b return sum # Function with parameters + docstring def sum_of_nums(a, b): '''prints the sum of two numbers''' # Docstring sum = a + b print(sum) # Calling the functions: print(add_two_numbers()) # 20 sum_of_nums(10, 20) # 30
return statement, it automatically returns None. Always check whether you need the result back or just the side effect (like printing).
help(function_name) to see it. From file: help(print) shows print's built-in docstring.
Arguments matched by position — first arg → first param, second → second, etc.
def subtract(a, b): print(a - b) subtract(10, 3) # a=10, b=3 → prints 7
Arguments passed using parameter names — position doesn't matter.
def greet(name, message): print(message, name) greet(message="Hello", name="Alice") # Hello Alice
Parameters with a default value. If no argument is passed for that parameter, the default is used.
def greet(name, message="Hello"): # message has default print(message, name) greet("Alice") # Hello Alice (uses default) greet("Bob", "Hi") # Hi Bob (overrides default)
def f(a=10, b) is a SyntaxError. From file: def sum(b, a=10) — default a comes after non-default b.
*args)Use when the number of arguments is not known in advance. Collects all positional arguments into a tuple.
def total(*args): result = 0 for num in args: result += num return result print(total(1, 2, 3, 5)) # 11
**kwargs)Use when arguments come with names and the count is unknown. Collects into a dictionary.
def display(**kwargs): for key, value in kwargs.items(): print(key, "=", value) display(name="Alice", age=20) # name = Alice # age = 20
A variable's scope determines where it can be accessed. Python searches for a variable name in this order: L → E → G → B
| Letter | Scope | Where |
|---|---|---|
| L | Local | Inside the current function |
| E | Enclosing | Inside outer (enclosing) function |
| G | Global | Top-level module code |
| B | Built-in | Python's built-in names (print, len, etc.) |
a = 20 # Global def demo(): a = 10 # Enclosing (for inner functions) def sample(): nonlocal a a = 5 # modifies enclosing a print("inner", a) # 5 sample() print("enclosing", a) # 5 (changed by nonlocal) demo() print("outer", a) # 20 (global unchanged)
nonlocal to modify the enclosing function's variable. Without it, assignment creates a new local variable instead.
A lambda is a single-line anonymous function. Defined using the lambda keyword. Used for short, throwaway operations.
lambda arguments : expression
From your file:
# Equivalent functions: square_lambda = lambda x : x * x def square(x): return x * x print(square_lambda(5)) # 25 # Lambda used inline (from file): number = int(input("Enter the number: ")) print("square =", (lambda x : x * x)(number)) # Two arguments: add = lambda x, y : x + y print(add(3, 4)) # 7
Recursion is a technique where a function calls itself. Every recursive function must have two parts:
Classic example from your file — factorial:
# n! = n × (n-1)! # 5! = 5 × 4! = 5 × 4 × 3! = ... = 120 def factorial(number): if number == 0 or number == 1: # Base case return 1 else: # Recursive case return number * factorial(number - 1) print(factorial(5)) # 120
Trace of factorial(5) — how the calls unfold (from trace table):
| Call | number | Returns |
|---|---|---|
| factorial(5) | 5 | 5 × factorial(4) |
| factorial(4) | 4 | 4 × factorial(3) |
| factorial(3) | 3 | 3 × factorial(2) |
| factorial(2) | 2 | 2 × factorial(1) |
| factorial(1) | 1 | 1 (base case!) |
Unwinding: 1 → 2×1=2 → 3×2=6 → 4×6=24 → 5×24=120
calculator(a, b, operation) that takes two numbers and an operation string ("add", "sub", "mul", "div") and returns the result.
def calculator(a, b, operation="add"): if operation == "add": return a + b elif operation == "sub": return a - b elif operation == "mul": return a * b elif operation == "div": return a / b print(calculator(10, 5, "mul")) # 50 print(calculator(10, 3)) # 13 (default add)
total(*nums) that accepts any number of integers and returns their sum.
Test it with 3 different calls.
def total(*nums): result = 0 for n in nums: result += n return result print(total(1, 2, 3)) # 6 print(total(10, 20, 30, 40)) # 100 print(total(5)) # 5
F = C × 9/5 + 32# 1. Celsius to Fahrenheit to_fahrenheit = lambda c : c * 9/5 + 32 print(to_fahrenheit(100)) # 212.0 # 2. Check even is_even = lambda n : n % 2 == 0 print(is_even(4)) # True print(is_even(7)) # False # 3. Larger of two bigger = lambda a, b : a if a > b else b print(bigger(5, 9)) # 9
power(base, exp) that calculates base ** exp without using the ** operator.
base^n = base × base^(n-1), base case: exp == 0 returns 1.
def power(base, exp): if exp == 0: # Base case: anything^0 = 1 return 1 else: # Recursive case return base * power(base, exp - 1) print(power(2, 3)) # 8 (2×2×2) print(power(5, 0)) # 1 print(power(3, 4)) # 81
a = 20
def demo():
a = 10
def sample():
nonlocal a
a = 5
print("inner", a)
sample()
print("enclosing", a)
demo()
print("outer", a)
# inner 5 — sample() sets nonlocal a to 5
# enclosing 5 — nonlocal changed demo()'s a from 10 to 5
# outer 20 — global a was never touched
return statement?*args collect arguments into?nonlocal keyword do?