Functions
Functions are one of the most important parts of programming.
A function is a named block of code that performs a specific task and can be reused multiple times.
Instead of writing the same code again and again, we define a function once and call it whenever needed.
Functions enable:
- Code reusability
- Code organization and modularity
- Easier debugging and testing
- Abstraction of complexity
Defining Functions using def
Section titled “Defining Functions using def”Functions are defined using the def keyword.
def greet(name): print(f"Hello, {name}!")Function Components:
def→ keyword that defines a functiongreet→ function name (follow naming conventions: lowercase_with_underscores)name→ parameter (input to the function)- Function body → indented statements
To run the function, we call it:
greet("Alice") # Output: Hello, Alice!Professional Practices:
- Use descriptive function names that indicate what they do
- Keep functions focused on a single responsibility (SRP)
- Add docstrings to document your functions
- Use type hints for clarity (Python 3.5+)
How function call works internally
Section titled “How function call works internally”When a function is called, Python follows a clear process:
- It creates a new frame (memory space) for that function
- It assigns arguments to parameters
- It executes the function body
- It returns the result (if any)
- It removes the function from memory
Call Flow Diagram
Section titled “Call Flow Diagram”Call Stack
Section titled “Call Stack”Python uses a stack to manage function calls.
- Last function called → executed first
- When function finishes → removed from stack
Example
Section titled “Example”def f1(): print("f1 start") f2() print("f1 end")
def f2(): print("f2 running")
f1()Call Stack Visualization
Section titled “Call Stack Visualization”Stack Representation
Section titled “Stack Representation”Top of Stack------------f2()f1()------------BottomAfter f2() finishes:
Top of Stack------------f1()------------BottomParameters and Return Values
Section titled “Parameters and Return Values”Functions can take inputs (parameters) and return outputs (return values).
def add(a, b): return a + b
result = add(5, 3)print(result) # Output: 8Key Concepts:
- Parameters → inputs to the function (
a,b) - Arguments → actual values passed to the function (
5,3) - Return → output sent back to caller (
a + b) - If no return statement → Python returns
None
Return Value Semantics:
def divide(a, b): if b == 0: return None # Error case return a / b
result = divide(10, 2)print(result) # 5.0Multiple Return Values:
def get_coordinates(): return 10, 20 # Returns a tuple
x, y = get_coordinates()print(x, y) # Output: 10 20Default Arguments
Section titled “Default Arguments”You can provide default values to parameters, making them optional.
def greet(name="Guest"): print(f"Hello, {name}")
greet() # Uses default: Hello, Guestgreet("Sahil") # Overrides default: Hello, SahilKey Characteristics:
- Default parameters must come after non-default parameters
- If no value is passed → default is used
- If value is passed → default is ignored
Example with Mixed Parameters:
def create_user(username, email, role="user"): print(f"User: {username}, Email: {email}, Role: {role}")
create_user("alice", "alice@example.com") # role="user"create_user("bob", "bob@example.com", role="admin") # role="admin"Variable Arguments: *args, **kwargs
Section titled “Variable Arguments: *args, **kwargs”Sometimes you don’t know in advance how many arguments will be passed.
*args (Arbitrary Positional Arguments)
Section titled “*args (Arbitrary Positional Arguments)”Allows passing any number of positional arguments to a function.
def total(*args): print(f"Args: {args}") # args is a tuple return sum(args)
print(total(1, 2, 3, 4)) # Output: 10print(total(5, 10, 15)) # Output: 30Key Points:
*argspacks arguments into a tuple- Must come after regular parameters
- Name
argsis convention (could be*numbers, etc.)
**kwargs (Arbitrary Keyword Arguments)
Section titled “**kwargs (Arbitrary Keyword Arguments)”Allows passing any number of keyword arguments to a function.
def show(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}")
show(name="Alice", age=30, city="NYC")# Output:# name: Alice# age: 30# city: NYCKey Points:
**kwargspacks arguments into a dictionary- Must come after
*argsand regular parameters - Name
kwargsis convention (could be**options, etc.)
Combining All Parameter Types
Section titled “Combining All Parameter Types”def process(required, default="value", *args, **kwargs): print(f"Required: {required}") print(f"Default: {default}") print(f"Args: {args}") print(f"Kwargs: {kwargs}")
process(1, 2, 3, 4, name="Alice", role="admin")# Output:# Required: 1# Default: 2# Args: (3, 4)# Kwargs: {'name': 'Alice', 'role': 'admin'}Recursion
Section titled “Recursion”A function calling itself is called recursion. Essential for divide-and-conquer algorithms.
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
print(factorial(5)) # Output: 120Recursion Components:
- Base case → condition that stops recursion (e.g.,
n == 1) - Recursive case → function calls itself with modified argument
How It Works for factorial(3):
factorial(3) = 3 * factorial(2) = 3 * 2 * factorial(1) = 3 * 2 * 1 = 6Professional Considerations:
- Risk of stack overflow if recursion is too deep
- Python has a recursion limit (check with
sys.getrecursionlimit()) - Not always faster than iterative solutions
- Use for naturally recursive problems (tree traversal, divide-and-conquer)
Execution Flow
Section titled “Execution Flow”Call Stack for Recursion
Section titled “Call Stack for Recursion”factorial(3)factorial(2)factorial(1)Then it resolves back step by step.
Lambda Functions
Section titled “Lambda Functions”Lambda functions are small, unnamed functions written in a single line. It is also known as an anonymous function.
add = lambda x, y: x + yprint(add(2, 3)) # Output: 5Syntax: lambda parameters: expression
Characteristics:
- Anonymous (no name binding required)
- Single expression only (no statements)
- Used for short operations
- Implicitly returns the expression result
Common Use Cases:
# With map()numbers = [1, 2, 3, 4]squared = list(map(lambda x: x**2, numbers))print(squared) # [1, 4, 9, 16]
# With filter()even_numbers = list(filter(lambda x: x % 2 == 0, numbers))print(even_numbers) # [2, 4]
# With sorted()students = [("Alice", 90), ("Bob", 85), ("Charlie", 95)]sorted_by_score = sorted(students, key=lambda x: x[1])Higher-Order Functions: map(), filter(), reduce()
Section titled “Higher-Order Functions: map(), filter(), reduce()”These functions take another function as an argument or return a function.
Applies a function to all items in an iterable.
numbers = [1, 2, 3]result = list(map(lambda x: x * 2, numbers))print(result) # [2, 4, 6]Use Case: Transform all elements consistently
filter()
Section titled “filter()”Selects items based on a condition (function returns True/False).
numbers = [1, 2, 3, 4, 5]result = list(filter(lambda x: x % 2 == 0, numbers))print(result) # [2, 4]Use Case: Keep only elements matching a condition
reduce()
Section titled “reduce()”Combines values into a single result through repeated application.
from functools import reduce
numbers = [1, 2, 3, 4]result = reduce(lambda x, y: x + y, numbers)print(result) # 10 (1+2+3+4)Use Case: Aggregate values (sum, product, concatenation)
# Old wayresult = list(map(lambda x: x * 2, numbers))
# Modern way (more readable):::noteMany prefer list comprehensions for readability::::Command-Line Arguments
Section titled “Command-Line Arguments”Arguments passed from the terminal when running a script.
import sys
print(sys.argv)Running from Terminal:
python script.py hello worldOutput:
['script.py', 'hello', 'world']How sys.argv Works:
sys.argv[0]→ script name (script.py)sys.argv[1]→ first argument (hello)sys.argv[2]→ second argument (world)
Practical Example:
import sys
if len(sys.argv) < 2: print("Usage: python script.py <name>") sys.exit(1)
name = sys.argv[1]print(f"Hello, {name}!")Professional Approach: Use argparse library for advanced argument parsing:
import argparse
parser = argparse.ArgumentParser(description='Greeting script')parser.add_argument('name', help='Name to greet')parser.add_argument('--greeting', default='Hello', help='Greeting word')
args = parser.parse_args()print(f"{args.greeting}, {args.name}!")Scope of Variables
Section titled “Scope of Variables”Python follows the LEGB rule for variable resolution:
- Local → Variables inside current function
- Enclosing → Variables in enclosing function (nested functions)
- Global → Variables at module level
- Built-in → Python’s built-in names
Example:
x = "global" # Global scope
def outer(): x = "enclosing" # Enclosing scope
def inner(): x = "local" # Local scope print(x)
inner() print(x)
outer()# Output:# local# enclosingProfessional Knowledge:
- Python searches for variables in LEGB order
- First found match is used
- Helps understand variable access and modification
Common Issues:
x = 10
def modify(): x = x + 1 # UnboundLocalError!
modify()# Error: local variable 'x' referenced before assignmentScope Diagram
Section titled “Scope Diagram”global and nonlocal Keywords
Section titled “global and nonlocal Keywords”global
Section titled “global”Declare that a function uses a global variable, allowing modification.
x = 10
def change(): global x # Declare intent to modify global x x = 20
change()print(x) # Output: 20Use Case: When you need to modify a global state (generally avoid this)
nonlocal
Section titled “nonlocal”Declare that a function uses a variable from an enclosing function.
def outer(): x = 10
def inner(): nonlocal x # Declare intent to modify enclosing x x = 20
inner() print(x) # Output: 20
outer()Use Cases:
- Closures that need to modify captured variables
- Nested function scope modification
Return Statement
Section titled “Return Statement”The return statement does two things:
- Sends a value back to the caller
- Stops execution of the function immediately
def check(n): if n > 0: return "Positive" return "Negative"
print(check(5)) # Output: Positiveprint(check(-3)) # Output: NegativeImportant Behaviors:
def example(): print("Start") return "Result" print("This won't execute") # Unreachable code
example()# Output: StartMultiple Returns:
def validate(x): if x < 0: return "Negative" if x == 0: return "Zero" return "Positive"Early Return Pattern:
def process(data): if not data: return None # Early exit
# Process data... return resultDocstrings
Section titled “Docstrings”Documentation strings that describe what a function does. Essential for professional code.
def add(a, b): """Returns sum of two numbers""" return a + bAccess Docstring:
print(add.__doc__) # Returns: "Returns sum of two numbers"help(add) # Displays docstring with other infoComprehensive Docstring:
def calculate_average(numbers): """Calculate the average of a list of numbers.
Args: numbers (list): A list of numeric values.
Returns: float: The average of the numbers.
Raises: ValueError: If the list is empty.
Examples: >>> calculate_average([1, 2, 3]) 2.0 """ if not numbers: raise ValueError("List cannot be empty") return sum(numbers) / len(numbers)Docstring Formats:
- Google style (recommended)
- NumPy style
- Sphinx style
Professional Practice: Every public function should have a docstring explaining its purpose, parameters, return value, and any exceptions it raises.
Pure and Impure Functions
Section titled “Pure and Impure Functions”Understanding the difference helps write testable, predictable code.
Pure Function
Section titled “Pure Function”A function that:
- Always returns the same output for the same input
- Has no side effects (doesn’t modify external state)
- Same input → same output
- No side effects
- Does not modify external state (i.e. it does not change any variables outside its scope)
def add(x, y): """Pure function: always returns same result""" return x + y
print(add(2, 3)) # Always 5print(add(2, 3)) # Always 5Benefits:
- Easy to test
- Predictable behavior
- Cacheable results
- Thread-safe
Impure Function
Section titled “Impure Function”A function that:
- Can return different outputs for the same input
- Modifies external state (side effects)
- Changes external state
- Can return different outputs for same input
- Has side effects (modifies global variable
count)
count = 0
def increment(): global count count += 1 return count
print(increment()) # Returns 1print(increment()) # Returns 2Side Effects Include:
- Modifying global variables
- File I/O operations
- Network requests
- Database modifications
- Printing to console
Professional Practice: Minimize impure functions. Separate side effects from business logic for better testability.
Pure function
Section titled “Pure function”def add(x, y): return x + yImpure function
Section titled “Impure function”count = 0
def inc(): global count count += 1