Python Functions: Building Modular and Reusable Code
In Python, a function is a block of organized, reusable code that is used to perform a single,
related action. Functions provide better modularity for your application and a high degree of
code reusing.
1. Python Functions :
- A function is a block of code.
- Block of statements that perform a specific task.
- Functions are defined using the def keyword.
- A function can return data as a result.
- To call function, use the function name followed by parentheses ( ).
- A colon ( : ) is used after the function definition, and the code block forming the function's body must be indented.
Syntax:
def function_name(parameters):
"""Docstring: Optional, explains what the function does."""
# Function body (indented code block)
# ...
return value # Optional: returns a value
Key Points :
- def: Keyword to define a function.
- function_name: A unique name that follows Python's naming conventions (lowercase with words separated by underscores).
- parameters: Optional. These are placeholders for values that the function will receive when called.
- : - A colon marks the end of the function header.
- Indentation: The function body must be indented (typically 4 spaces).
- Docstring: A string literal that immediately follows the function definition.
It's used to document the function's purpose, arguments, and return value. Highly
recommended for good practice.
- return statement: Optional. It sends a value back to the caller. If return is omitted or
used without an argument, the function implicitly returns None.
Example 1.1: A simple function without parameters or return value :
def greet():
"""Prints a simple greeting message."""
print("Hello, Python learner!")
# Calling the function
greet()
2. Calling a Function :
To execute a function, you simply use its name followed by parentheses ( ). If the function expects
arguments, you pass them inside the parentheses.
Example 2.1: Calling the greet function :
greet() # Output: Hello, Python learner!
3. Function Parameters and Arguments :
- Parameters: Variables listed inside the parentheses in the function definition. They are placeholders.
- Arguments: The actual values passed to the function when it is called.
Example 3.1: Function with one parameter
def greet_person(name):
"""Greets a person by their name."""
print(f"Hello, {name}!")
greet_person("Alice") # "Alice" is the argument
greet_person("Bob") # "Bob" is the argument
4. The return Statement :
The return statement is used to send a value back from the function to the place where it was called.
This allows functions to produce results that can be used in other parts of your program.
Example 4.1: Function returning a value
def add(a, b):
"""Adds two numbers and returns their sum."""
sum_result = a + b
return sum_result
# Calling the function and storing the returned value
result = add(5, 3)
print(f"The sum is: {result}") # Output: The sum is: 8
# Using the returned value directly
print(f"10 + 20 = {add(10, 20)}") # Output: 10 + 20 = 30
Example 4.2: Function returning multiple values (as a tuple)
Functions can implicitly return multiple values by separating them with commas. Python packs them into a tuple.
def calculate_metrics(numbers):
"""Calculates sum and average of a list of numbers."""
total = sum(numbers)
count = len(numbers)
if count == 0:
return 0, 0 # Return 0 for both if list is empty
average = total / count
return total, average # Returns a tuple (total, average)
data = [10, 20, 30, 40, 50]
total_sum, avg_value = calculate_metrics(data) # Unpacking the returned tuple
print(f"Sum: {total_sum}, Average: {avg_value}")
empty_data = []
s, a = calculate_metrics(empty_data)
print(f"Sum (empty): {s}, Average (empty): {a}")
5. Types of Arguments :
Python supports several types of arguments that can be used in function calls:
5.1. Positional Arguments :
Arguments passed in the order they are defined in the function signature. The position matters.
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"Its name is {pet_name}.")
describe_pet("dog", "Buddy") # Positional: "dog" maps to animal_type, "Buddy" to pet_name
# describe_pet("Buddy", "dog") # Incorrect order will lead to wrong output
5.2. Keyword Arguments :
Arguments identified by their parameter names in the function call. Order doesn't matter, but the name must match.
describe_pet(pet_name="Max", animal_type="cat") # Keyword arguments
describe_pet(animal_type="fish", pet_name="Nemo") # Order doesn't matter
5.3. Default Arguments :
Parameters can have default values. If an argument is not provided for such a parameter, its default value is used. Default arguments must come after any non-default arguments.
def greet_with_default(name="Guest"): # "Guest" is the default value
print(f"Hello, {name}!")
greet_with_default("Charlie") # Output: Hello, Charlie!
greet_with_default() # Output: Hello, Guest! (uses default)
Caution with Mutable Default Arguments:
Be careful when using mutable objects (like lists or dictionaries) as default arguments. They are created only once when the function is defined, not on each call.
def add_to_list(item, my_list=[]): # DANGER! my_list is created once
my_list.append(item)
return my_list
list1 = add_to_list(1)
print(list1) # Output: [1]
list2 = add_to_list(2)
print(list2) # Output: [1, 2] - Oops! list2 modified list1's default list!
# Correct way to handle mutable defaults:
def add_to_list_safe(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
list3 = add_to_list_safe(1)
print(list3) # Output: [1]
list4 = add_to_list_safe(2)
print(list4) # Output: [2] - Correct!
6. Arbitrary Arguments (*args and **kwargs) :
These allow functions to accept a variable number of arguments.
6.1. *args (Arbitrary Positional Arguments) :
*args allows a function to accept any number of positional arguments. These arguments are packed into a tuple.
def sum_all_numbers(*numbers):
"""Sums all numbers passed as arguments."""
total = 0
for num in numbers:
total += num
return total
print(sum_all_numbers(1, 2, 3)) # Output: 6
print(sum_all_numbers(10, 20, 30, 40)) # Output: 100
print(sum_all_numbers()) # Output: 0
6.2. **kwargs (Arbitrary Keyword Arguments) :
**kwargs allows a function to accept any number of keyword arguments. These arguments are packed into a dictionary.
def print_user_info(**info):
"""Prints user information from keyword arguments."""
print("User Info:")
for key, value in info.items():
print(f" {key.replace('_', ' ').title()}: {value}")
print_user_info(name="Eve", age=28, city="Paris")
print_user_info(product="Laptop", price=1200, category="Electronics", brand="XYZ")
Order of Arguments in Function Definition :
If you use a mix of argument types, the order in the function definition matters :
- Positional-only parameters (Python 3.8+ with /)
- Positional or keyword parameters
- *args
- Keyword-only parameters (after *args or *)
- **kwargs
Example of mixed arguments :
def complex_function(a, b, *args, kw_only1, kw_only2=None, **kwargs):
print(f"a: {a}, b: {b}")
print(f"args: {args}")
print(f"kw_only1: {kw_only1}, kw_only2: {kw_only2}")
print(f"kwargs: {kwargs}")
# complex_function(1, 2, 3, 4, kw_only1="X", kw_only2="Y", extra="Z")
7. Scope of Variables (LEGB Rule) :
Understanding variable scope is crucial: where a variable is defined determines where it can be accessed. Python follows the LEGB rule:
- Local : Variables defined inside a function.
- Enclosing function locals : Variables in the local scope of enclosing functions (for nested functions).
- Global : Variables defined at the top level of a script or module.
- Built-in : Names pre-defined in Python (e.g., print, len).
Example 7.1 : Local vs Global scope
global_var = "I am a global variable." # Global scope
def my_function():
local_var = "I am a local variable." # Local scope
print(local_var)
print(global_var) # Can access global_var
my_function()
# print(local_var) # This would cause a NameError because local_var is not defined in global scope
print(global_var)
global keyword :
You can modify a global variable from inside a function using the global keyword.
counter = 0 # Global
def increment_counter():
global counter # Declare intent to modify global 'counter'
counter += 1
print(f"Counter inside function: {counter}")
increment_counter() # Output: Counter inside function: 1
increment_counter() # Output: Counter inside function: 2
print(f"Counter outside function: {counter}") # Output: Counter outside function: 2
nonlocal keyword (for nested functions) :
Used to modify variables in the nearest enclosing scope that is not global.
def outer_function():
x = "outer"
def inner_function():
nonlocal x # Refers to 'x' in outer_function's scope
x = "inner"
print(f"Inner: {x}")
inner_function()
print(f"Outer: {x}") # 'x' has been modified by inner_function
outer_function()
8. First-Class Functions (Advanced) :
In Python, functions are "first-class citizens." This means they can be:
- Assigned to variables.
- Passed as arguments to other functions.
- Returned as values from other functions.
- Stored in data structures (lists, dictionaries).
Example 8.1: Assigning a function to a variable
def say_hello(name):
return f"Hello, {name}!"
my_greeting_func = say_hello # Assigning the function itself to a variable
print(my_greeting_func("David")) # Calling it via the variable
Example 8.2: Passing a function as an argument (Higher-Order Functions)
def apply_operation(func, a, b):
return func(a, b)
def multiply(x, y):
return x * y
def power(x, y):
return x ** y
result1 = apply_operation(multiply, 4, 5) # Passing 'multiply' function
print(f"Multiply result: {result1}") # Output: 20
result2 = apply_operation(power, 2, 3) # Passing 'power' function
print(f"Power result: {result2}") # Output: 8
9. Lambda Functions (Anonymous Functions) :
Lambda functions are small, anonymous functions defined with the lambda keyword. They can only contain a single expression and are often used for short, one-time operations.
lambda arguments: expression
Example 9.1: Basic lambda function
add_one = lambda x: x + 1
print(add_one(5)) # Output: 6
# Used with higher-order functions like map(), filter(), sorted()
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
print(f"Squared: {squared_numbers}") # Output: [1, 4, 9, 16, 25]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Even: {even_numbers}") # Output: [2, 4]
10. Recursion (Advanced) :
A function is recursive if it calls itself directly or indirectly to solve a problem. Recursion is often used for problems that can be broken down into smaller, similar subproblems.
Key Points :
- Base Case: Every recursive function must have a base case that stops the recursion to prevent infinite loops.
- Recursive Step: The part where the function calls itself with a modified input, moving towards the base case.
Example 10.1: Factorial using recursion
def factorial(n):
"""Calculates the factorial of a non-negative integer using recursion."""
if n == 0 or n == 1: # Base case
return 1
else: # Recursive step
return n * factorial(n - 1)
print(f"Factorial of 5: {factorial(5)}") # 5 * 4 * 3 * 2 * 1 = 120
print(f"Factorial of 0: {factorial(0)}") # Output: 1
Caution with Recursion :
Deep recursion can lead to a RecursionError (maximum recursion depth exceeded) because each function
call adds to the call stack. Python has a default recursion limit (usually around 1000-3000).
For very large inputs, iterative solutions are often preferred over recursive ones for performance and memory reasons.
Best Practices for Functions :
- Single Responsibility Principle (SRP) : Each function should do one thing and do it well.
- Meaningful Names : Function names should clearly indicate what they do.
- Docstrings : Always write docstrings to explain your functions.
- Comments: Use comments for complex logic within the function, but good code often needs fewer
comments if it's well-named and structured.
- Avoid Side Effects (where possible) : Functions that modify global state or input arguments
unexpectedly can make code harder to reason about. Aim for pure functions (same input, same output,
no side effects) when feasible.
- Parameter Type Hinting (Python 3.5+) : Use type hints to indicate the expected types of arguments
and return values. This improves readability and allows static analysis tools to catch errors.
def add_numbers(a: int, b: int) -> int:
"""Adds two integers and returns their sum."""
return a + b
5 Basic Python Function Examples :
Example 1: Function without parameters
def greet():
print("Hello, welcome to Python!")
greet()
#output - Hello, welcome to Python!
Example 2: Function with parameters
def greet_user(name):
print("Hello,", name)
greet_user("Mr. Shankar")
#output - Hello, Mr. Shankar
Example 3: Function that returns a value
def add(a, b):
return a + b
result = add(5, 3)
print("Sum is:", result)
#output - Sum is: 8
Example 4: Function with default parameter
def greet(name="Guest"):
print("Hello,", name)
greet() # uses default
greet("Anita") # custom value
#output -
'''
Hello, Guest
Hello, Anita
'''
Example 5: Function with a loop inside
def print_table(n):
for i in range(1, 11):
print(f"{n} x {i} = {n*i}")
print_table(5)
#output -
'''
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
...
5 x 10 = 50
'''