Python Basics - 04. Functions¶
Functions are reusable blocks of code that make programs cleaner, easier to test, and easier to maintain.
In this notebook, you will learn:
how to define and call functions
how different parameter types work
how return values are used
common pitfalls and best practices
practical advanced patterns like recursion and decorators
Download Notebook¶
1. Defining and Calling Functions¶
Use the def keyword to define a function. A function may take input parameters and may return a value.
def greet(name):
# Return a formatted greeting message
return f"Hello, {name}!"
print(greet("Python"))
print(greet("Alice"))
Hello, Python!
Hello, Alice!
2. Parameters: Positional, Keyword, and Default Values¶
You can pass arguments in several ways:
positional arguments: matched by position
keyword arguments: matched by parameter name
default values: used when an argument is not provided
def power(base, exponent=2):
# default exponent=2 means square by default
return base ** exponent
print(power(3)) # positional, uses default exponent
print(power(2, 3)) # positional arguments
print(power(base=4, exponent=2)) # keyword arguments
9
8
16
3. Return Values and Multiple Results¶
Functions can return any object, including tuples for multiple values.
def divide_and_remainder(a, b):
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = divide_and_remainder(17, 5)
print(f"quotient={q}, remainder={r}")
quotient=3, remainder=2
4. Variable Scope (Local vs Global)¶
Local variables exist only inside a function.
Global variables are defined outside functions.
Prefer passing values through parameters instead of relying on globals.
message = "global"
def show_scope():
message = "local"
print("Inside function:", message)
show_scope()
print("Outside function:", message)
Inside function: local
Outside function: global
5. Recursion¶
A recursive function calls itself with a smaller subproblem. Every recursive function must have a base case.
def factorial(n):
if n < 0:
raise ValueError("n must be non-negative")
if n in (0, 1):
return 1
return n * factorial(n - 1)
print(factorial(5))
120
6. Variable-Length Arguments: *args and **kwargs¶
*argscollects positional arguments into a tuple.**kwargscollects keyword arguments into a dictionary.
def sum_all(*args):
return sum(args)
def show_profile(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print(sum_all(1, 2, 3, 4, 5))
show_profile(name="Alice", role="Engineer", level="Beginner")
15
name: Alice
role: Engineer
level: Beginner
7. Lambda Functions and Decorators (Step by Step)¶
Lambda functions¶
A lambda is a short anonymous function written in one line.
Use lambda when:
the logic is very small
you only need the function once
readability is still clear
Example pattern:
regular function:
def square(x): return x * xlambda version:
lambda x: x * x
Decorators¶
A decorator is a function that takes another function and returns a new function with extra behavior.
You can use decorators to:
log function calls
measure runtime
check permissions/inputs
avoid repeating the same pre/post logic in many functions
Think of it as “wrapping” a function with additional steps before and after it runs.
# ---------- Part A: Lambda (easy comparison) ----------
# Regular function version
def square_def(x):
return x * x
# Lambda version (same logic, shorter syntax)
square_lambda = lambda x: x * x
print("square_def(6):", square_def(6))
print("square_lambda(6):", square_lambda(6))
# ---------- Part B: Decorator step by step ----------
# Step 1) A normal function
def add(a, b):
return a + b
print("\nNormal add:", add(10, 20))
# Step 2) A decorator that adds logging around any function
def log_call(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
# Step 3) Manual wrapping (without @ syntax)
logged_add_manual = log_call(add)
print("\nManual wrapped add:")
logged_add_manual(3, 5)
# Step 4) Decorator syntax sugar (@log_call)
@log_call
def multiply(a, b):
return a * b
print("\n@decorator wrapped multiply:")
multiply(4, 6)
square_def(6): 36
square_lambda(6): 36
Normal add: 30
Manual wrapped add:
[LOG] Calling add with args=(3, 5), kwargs={}
[LOG] add returned 8
@decorator wrapped multiply:
[LOG] Calling multiply with args=(4, 6), kwargs={}
[LOG] multiply returned 24
24