Functions
Phase 1: The Basics (Levels 1–5)
Level 1: Defining a Simple Function [6]
Explanation: Use the def keyword, followed by the function name, parentheses () ,
and a colon : . The code block inside must be indented. [7, 8, 9]
def say_hello():
print("Hello, Python learner!")
say_hello()
Level 2: Functions with Parameters and Arguments
Explanation: Parameters are placeholders defined in the function signature.
Arguments are the actual values passed into those placeholders during a call. [10, 11, 12]
def greet_user(username):
print(f"Welcome back, {username}!")
greet_user("Alice")
greet_user("Bob")
Level 3: Returning Values from Functions [13, 14]
Explanation: The return statement exits a function and sends a value back to the
caller. Without a return , Python functions automatically return None . [15, 16, 17, 18]
def add_numbers(num1, num2):
return num1 + num2
result = add_numbers(15, 25)
print(f"The total sum is: {result}")
Level 4: Positional vs. Keyword Arguments
Explanation: Positional arguments depend strictly on the order they are passed.
Keyword arguments use the parameter names directly, allowing you to pass them in any
order.
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type} named {pet_name}.")
describe_pet("dog", "Rex")
# Keyword arguments: Order does not matter
describe_pet(pet_name="Whiskers", animal_type="cat")
Level 5: Default Parameter Values
Explanation: You can assign default values to parameters. If no argument is passed for
that parameter during execution, the default value is used automatically. [22, 23, 24]
def calculate_price(total, tax_rate=0.05): # tax_rate defaults
to 0.05
return total + (total * tax_rate)
# Uses the default tax rate
print(calculate_price(100))
# Overrides the default tax rate with 0.12
print(calculate_price(100, 0.12))
Phase 2: Intermediate Concepts (Levels 6–10)
Level 6: Variable-Length Positional Arguments ( *args ) [25]
Explanation: Use *args to allow a function to accept any number of positional
arguments. Inside the function, args acts as a tuple containing all passed values. [26, 27,
28]
def sum_all_numbers(*args):
# args behaves exactly like a tuple: (1, 2, 3, 4)
return sum(args)
print(sum_all_numbers(1, 2, 3, 4))
print(sum_all_numbers(50, 100))
Level 7: Variable-Length Keyword Arguments ( **kwargs )
Explanation: Use **kwargs to accept an arbitrary number of keyword arguments.
Inside the function, kwargs acts as a dictionary of key-value pairs.
def build_profile(first, last, **user_info):
# user_info behaves exactly like a dictionary
user_info['first_name'] = first
user_info['last_name'] = last
return user_info
profile = build_profile("John", "Doe", age=28,
city="Faisalabad", job="Engineer")
print(profile)
Level 8: Local vs. Global Variable Scope
Explanation: Variables created inside a function are local to that function. Variables
created in the main script are global. A function can read a global variable, but cannot
modify it unless explicit permission is granted. [32, 33, 34, 35]
message = "Global Message" # Global variable
def print_messages():
local_msg = "Local Message" # Local variable
print(local_msg)
print(message) # Accessing the global variable works
perfectly
print_messages()
# Printing local_msg here would cause a NameError
Level 9: Modifying Global Variables ( global keyword) [36]
Explanation: To alter a global variable's value from inside a local function scope,
declare it first using the global keyword. [37]
counter = 0 # Global variable
def increment():
global counter # Points explicitly to the global variable
counter += 1
increment()
increment()
print(f"Counter value: {counter}")
Level 10: Docstrings (Documentation Strings) [38, 39, 40]
Explanation: Triple quotes """ right below the function definition create a docstring.
This documents what your function does and can be accessed via help() or code
editors. [41, 42, 43, 44]
def compute_area(radius):
"""Calculates and returns the area of a circle.
Parameters:
radius (float): The radius of the circle
"""
import math
return [Link] * (radius ** 2)
print(compute_area.__doc__)
Phase 3: Advanced Concepts (Levels 11–15)
Level 11: Lambda Functions (Anonymous Functions) [45]
Explanation: High-speed, single-line anonymous functions created with the lambda
keyword. They are best used for temporary, small jobs. Syntax: lambda inputs:
output . [46, 47, 48, 49, 50]
# Simple lambda to square a number
square = lambda x: x * x
print(square(6))
# Lambda used for custom sorting criteria
pairs = [(1, 'one'), (3, 'three'), (2, 'two')]
[Link](key=lambda pair: pair[1]) # Sort elements by the
string name
print(pairs)
Level 12: Higher-Order Functions ( map and filter )
Explanation: Functions that accept other functions as arguments or return them as results
are called higher-order functions.
numbers = [1, 2, 3, 4, 5]
# map() applies a function to all items in an input list
squared = list(map(lambda x: x**2, numbers))
print(f"Squared list: {squared}")
# filter() extracts items that evaluate to True under a test
function
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Even items: {evens}")
Level 13: Recursion (Functions Calling Themselves) [56]
Explanation: A function that calls itself to solve smaller instances of the same problem.
Every recursive function requires a base case to prevent an infinite loop.
def get_factorial(n):
if n <= 1: # The Base Case
return 1
else: # The Recursive Case
return n * get_factorial(n - 1)
print(get_factorial(5)) # 5 * 4 * 3 * 2 * 1 = 120
Level 14: Positional-Only and Keyword-Only Arguments
Explanation: Use a forward slash / to force parameters before it to be positional-only.
Use an asterisk * to force parameters after it to be keyword-only.
def configure_system(mode, /, *, timeout, retry):
# mode -> must be positional
# timeout, retry -> must be named keywords
print(f"Mode: {mode}, Timeout: {timeout}, Retry: {retry}")
# Valid Call:
configure_system("production", timeout=30, retry=3)
# Invalid Call (Will throw TypeError):
# configure_system(mode="production", timeout=30, retry=3)
Level 15: Nested Functions and Closures
Explanation: A function defined inside another function is a nested function. A closure
is a nested function that retains access to the variables of its outer function even after the
outer function has finished executing.
def multiplier_factory(factor):
def multiply_by(number): # Nested function
return number * factor # Accesses 'factor' from parent
scope
return multiply_by # Returns the function logic itself
triple = multiplier_factory(3) # 'factor' is locked in as 3
print(triple(10)) # Outputs: 30
Phase 4: Professional Mastery (Levels 16–20)
Level 16: Decorators (Function Modifiers) [68]
Explanation: A decorator takes an existing function, wraps it with extra behavior, and
returns the modified function without permanently changing its core code.
def my_decorator(func):
def wrapper():
print("[Log] Something is happening BEFORE the function
runs.")
func()
print("[Log] Something is happening AFTER the function
runs.")
return wrapper
@my_decorator # Applying the decorator syntax
def say_cheese():
print("Cheese!")
say_cheese()
Level 17: Generators using the yield Keyword
Explanation: Functions containing the yield keyword instead of return are
generators. They produce items one at a time on-demand instead of saving massive
datasets directly into memory.
def countdown_timer(start_num):
while start_num > 0:
yield start_num # Pauses state and sends the current
value out
start_num -= 1
# Consuming the generator values on the fly
for number in countdown_timer(3):
print(number)
Level 18: Typing Hinting inside Functions
Explanation: Type hints do not change how code runs, but they provide static analysis
tools and other developers with the precise data types expected by a function.
from typing import List
def process_scores(names: List[str], base_score: int) ->
List[str]:
# Accepts a list of strings and an integer, returns a list
of strings
output = []
for name in names:
[Link](f"Student {name} scored {base_score}")
return output
print(process_scores(["Alex", "Sam"], 90))
Level 19: Managing States with the nonlocal Keyword
Explanation: Used inside nested functions to modify a variable belonging to the parent
local scope that is not global. [82, 83, 84, 85]
def running_total_tracker():
current_total = 0 # Enclosing scope variable
def add_to_total(amount):
nonlocal current_total # Links directly to parent tracking variable
current_total += amount
return current_total
return add_to_total
tracker = running_total_tracker()
print(tracker(10)) # Outputs: 10
print(tracker(25)) # Outputs: 35
Level 20: Advanced Decorators with Parameters
Explanation: By adding an extra outer layer of function wrapping, you can pass custom
configurations and settings directly into your python decorators. [86]
def repeat_execution(num_times):
def actual_decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return actual_decorator
@repeat_execution(num_times=3) # Tell the decorator to run this exactly 3
times
def ping_server():
print("Server pinged successfully...")
ping_server()