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 :


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 :



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 :


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 :

  1. Positional-only parameters (Python 3.8+ with /)
  2. Positional or keyword parameters
  3. *args
  4. Keyword-only parameters (after *args or *)
  5. **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:



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:



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 :


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 :

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
'''