Functions
Functions are one of the most important things you’ll learn in programming.
A function is simply a named block of code that does a specific job and can be used again and again whenever you need it.
Instead of writing the same code over and over, we write it once inside a function and just call that function whenever we need it.
Functions help you with:
- Reusing code instead of writing it again and again
- Keeping your code organized and broken into small parts
- Making debugging and testing easier
- Hiding complicated details behind a simple name
Defining Functions using def
Section titled “Defining Functions using def”You create a function using the def keyword.
def greet(name): print(f"Hello, {name}!")Parts of a Function:
def-> the keyword that tells Python you’re creating a functiongreet-> the name of the function (try to use lowercase_with_underscores style)name-> a parameter (this is the input the function takes)- Function body -> the indented lines of code that run when the function is called
To actually run the function, you call it like this:
greet("Alice") # Output: Hello, Alice!Good Habits to Follow:
- Give your functions clear names that explain what they do
- Try to make each function do just one job (this is called Single Responsibility)
- Add docstrings so others know what your function does
- Use type hints to make things clearer (works from Python 3.5 onward)
How a function call works behind the scenes
Section titled “How a function call works behind the scenes”When you call a function, Python follows a simple set of steps:
- It creates a new frame (a small space in memory) just for that function
- It matches the arguments you passed to the function’s parameters
- It runs the code inside the function
- It sends back a result (if there is one)
- It removes that function’s memory space once it’s done
Call Flow Diagram
Section titled “Call Flow Diagram”Call Stack
Section titled “Call Stack”Python keeps track of function calls using something called a stack.
- The function called last -> gets finished first
- Once a function is done -> it gets removed from the 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 (these are called parameters) and give back outputs (these are called return values).
def add(a, b): return a + b
result = add(5, 3)print(result) # Output: 8Key Ideas to Understand:
- Parameters -> the inputs that a function expects (
a,b) - Arguments -> the actual values you give to the function when calling it (
5,3) - Return -> the output that gets sent back to whoever called the function (
a + b) - If there’s no return statement -> Python automatically returns
None
How Return Values Work:
def divide(a, b): if b == 0: return None # Error case return a / b
result = divide(10, 2)print(result) # 5.0Returning More Than One Value:
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 give a parameter a default value, which makes it optional when calling the function.
def greet(name="Guest"): print(f"Hello, {name}")
greet() # Uses default: Hello, Guestgreet("Sahil") # Overrides default: Hello, SahilThings to Keep in Mind:
- Parameters with default values must come after the ones without default values
- If you don’t pass a value -> Python uses the default one
- If you do pass a value -> the default one gets ignored
Example Using a Mix of 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 ahead of time how many arguments someone will pass into your function.
*args (Any Number of Positional Arguments)
Section titled “*args (Any Number of Positional Arguments)”This lets you pass any number of positional arguments into 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: 30Things to Remember:
*argscollects all the extra arguments into a tuple- It must come after the normal parameters
- The name
argsis just a common convention (you could also write*numbers, etc.)
**kwargs (Any Number of Keyword Arguments)
Section titled “**kwargs (Any Number of Keyword Arguments)”This lets you pass any number of keyword arguments into 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: NYCThings to Remember:
**kwargscollects all the extra keyword arguments into a dictionary- It must come after
*argsand the normal parameters - The name
kwargsis just a common convention (you could also write**options, etc.)
Combining All Parameter Types Together
Section titled “Combining All Parameter Types Together”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”When a function calls itself, this is called recursion. It’s really important for solving problems using a divide-and-conquer approach.
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
print(factorial(5)) # Output: 120Parts of Recursion:
- Base case -> the condition that stops the recursion from continuing (like
n == 1) - Recursive case -> where the function calls itself again, usually with a slightly different value
How factorial(3) Actually Works:
factorial(3) = 3 * factorial(2) = 3 * 2 * factorial(1) = 3 * 2 * 1 = 6Things to Keep in Mind:
- If the recursion goes too deep, you can run into a stack overflow error
- Python has a limit on how deep recursion can go (you can check it with
sys.getrecursionlimit()) - Recursion is not always faster than just using a loop
- Use recursion for problems that are naturally recursive in nature (like going through a tree, or divide-and-conquer problems)
Execution Flow
Section titled “Execution Flow”Call Stack for Recursion
Section titled “Call Stack for Recursion”factorial(3)factorial(2)factorial(1)After this, it works its way back step by step.
Lambda Functions
Section titled “Lambda Functions”Lambda functions are tiny, nameless functions that you write in just one line. They’re also called anonymous functions.
add = lambda x, y: x + yprint(add(2, 3)) # Output: 5How it’s written: lambda parameters: expression
Things That Make Lambda Special:
- It doesn’t need a name
- It can only hold a single expression (no multiple statements)
- It’s mainly used for short and simple operations
- It automatically returns the result of the expression, without needing a
returnkeyword
Common Places You’ll See Lambda Used:
# 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 are functions that either take another function as input, or give back a function as output.
This applies a function to every single item in a sequence.
numbers = [1, 2, 3]result = list(map(lambda x: x * 2, numbers))print(result) # [2, 4, 6]When to Use It: When you want to change every item in the same way
filter()
Section titled “filter()”This picks out only the items that match a certain condition (the function should return True or False).
numbers = [1, 2, 3, 4, 5]result = list(filter(lambda x: x % 2 == 0, numbers))print(result) # [2, 4]When to Use It: When you want to keep only the items that match a certain condition
reduce()
Section titled “reduce()”This combines all the items into one single result, by applying an operation again and again.
from functools import reduce
numbers = [1, 2, 3, 4]result = reduce(lambda x, y: x + y, numbers)print(result) # 10 (1+2+3+4)When to Use It: When you want to combine values together, like adding them up, multiplying them, or joining them
# Old wayresult = list(map(lambda x: x * 2, numbers))
# Modern way (more readable)result = [x * 2 for x in numbers]Command-Line Arguments
Section titled “Command-Line Arguments”These are values you can pass in from the terminal when you run your script.
import sys
print(sys.argv)Running it from the Terminal:
python script.py hello worldOutput:
['script.py', 'hello', 'world']How sys.argv Works:
sys.argv[0]-> the name of the script (script.py)sys.argv[1]-> the first argument you typed (hello)sys.argv[2]-> the second argument you typed (world)
A 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}!")A Better Way to Do This: You can use the argparse library for more advanced handling of arguments:
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 something called the LEGB rule to figure out which variable you actually mean:
- Local -> Variables that exist inside the current function
- Enclosing -> Variables that exist in a function that wraps around the current one (used with nested functions)
- Global -> Variables that exist at the top level of your file
- Built-in -> Names that already come built into Python itself
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# enclosingGood to Know:
- Python looks for variables in this exact order: Local, then Enclosing, then Global, then Built-in
- It uses whichever one it finds first
- This helps you understand how variables get accessed and changed
A Common Mistake:
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”This tells Python that a function wants to use a global variable, and that it’s allowed to change it.
x = 10
def change(): global x # Declare intent to modify global x x = 20
change()print(x) # Output: 20When to Use It: When you need to change a value that exists outside the function, at the global level (though it’s usually better to avoid this if possible)
nonlocal
Section titled “nonlocal”This tells Python that a function wants to use a variable from a function that wraps around it (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()When to Use It:
- When you’re working with closures and need to change a variable that was captured from outside
- When you need to modify a variable that belongs to a function wrapping around your current one
Return Statement
Section titled “Return Statement”The return statement does two things at once:
- It sends a value back to whoever called the function
- It immediately stops the function from running any further
def check(n): if n > 0: return "Positive" return "Negative"
print(check(5)) # Output: Positiveprint(check(-3)) # Output: NegativeSomething Important to Notice:
def example(): print("Start") return "Result" print("This won't execute") # Unreachable code
example()# Output: StartUsing Multiple Return Statements:
def validate(x): if x < 0: return "Negative" if x == 0: return "Zero" return "Positive"Returning Early:
def process(data): if not data: return None # Early exit
# Process data... return resultDocstrings
Section titled “Docstrings”These are little notes written inside a function that explain what it does. They’re an important part of writing clean, professional code.
def add(a, b): """Returns sum of two numbers""" return a + bHow to View a Docstring:
print(add.__doc__) # Returns: "Returns sum of two numbers"help(add) # Displays docstring with other infoA More Detailed 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)Different Styles of Docstrings:
- Google style (this one is recommended)
- NumPy style
- Sphinx style
Good Habit to Build: Every function that others will use should have a docstring that explains what it does, what it needs (parameters), what it gives back (return value), and any errors it might raise.
Pure and Impure Functions
Section titled “Pure and Impure Functions”Knowing the difference between these two helps you write code that’s easier to test and predict.
Pure Function
Section titled “Pure Function”A function is “pure” when it:
- Always gives back the same output for the same input, every single time
- Doesn’t cause any side effects (it doesn’t change anything outside of itself)
- Same input -> same output, always
- No side effects at all
- Doesn’t change anything outside its own scope (meaning it doesn’t touch variables that live outside the function)
def add(x, y): """Pure function: always returns same result""" return x + y
print(add(2, 3)) # Always 5print(add(2, 3)) # Always 5Why Pure Functions Are Good:
- They’re easy to test
- They behave in a predictable way
- Their results can be saved and reused (cached)
- They’re safe to use across multiple threads at once
Impure Function
Section titled “Impure Function”A function is “impure” when it:
- Can give back different results even with the same input
- Changes something outside of itself (this is called a side effect)
- Changes things outside its own scope
- Can return different results for the same input
- Has a side effect here, since it changes the global variable
count
count = 0
def increment(): global count count += 1 return count
print(increment()) # Returns 1print(increment()) # Returns 2Examples of Side Effects:
- Changing global variables
- Reading or writing files
- Making network requests
- Changing data in a database
- Printing something to the console
Good Habit to Build: Try to keep impure functions to a minimum. Keep side effects separate from your main logic so your code is easier to test.