Python tutorials  /  Python Functions - Complete Guide with Examples
Chapter 7 · Python

Python Functions - Complete Guide with Examples

A function in Python is a reusable, named block of code designed to perform a specific task. It is defined once using the 'def' keyword and can be called (invoked) multiple times throughout a program, optionally accepting inputs (parameters) and returning an output (return value).

Think of a function like a small machine you build once. You give it some raw material (input/arguments), it processes that material internally, and it gives you back a finished product (return value). Instead of rebuilding the machine every time you need it, you just feed it new material and reuse it.

Consider an online food delivery app like CompileX Eats. Every time a customer places an order, the system needs to calculate the total bill including tax and delivery charges. Instead of writing the same calculation logic in the checkout page, the cart page, and the invoice generator, developers write a single function called 'calculate_total(price, tax_rate, delivery_fee)'. This function is called from all three places. If the tax calculation logic changes tomorrow (say, due to a new government policy), developers only need to update the code inside this one function instead of hunting through the entire codebase, saving hours of debugging and reducing the risk of inconsistent bills across the app.

Functions are essential because they promote code reusability, reduce duplication (DRY principle - Don't Repeat Yourself), improve readability, make debugging easier by isolating logic, and allow large problems to be broken down into smaller, manageable, testable units. Without functions, programs become long, repetitive, and extremely difficult to maintain as they grow in size.

  • Built-in Functions: Functions that come pre-defined with Python itself, such as print(), len(), type(), input(), and range(). These are ready to use without any import or definition.
  • User-Defined Functions: Functions that programmers create themselves using the 'def' keyword to perform custom, task-specific operations tailored to their program's needs.
  • Lambda (Anonymous) Functions: Small, single-expression functions defined using the 'lambda' keyword without a name, typically used for short, throwaway operations like sorting keys or quick callbacks.
  • Recursive Functions: Functions that call themselves within their own body to solve problems that can be broken down into smaller, similar sub-problems, such as calculating factorials or traversing tree structures.
  • Higher-Order Functions: Functions that either accept other functions as arguments or return a function as their result, commonly used in functional programming patterns like map(), filter(), and reduce().
def function_name(parameter1, parameter2=default_value, *args, **kwargs): """Optional docstring describing the function""" # function body statement(s) return value # optional # Calling the function function_name(argument1, argument2)
Imagine you are building a grading system for a school. You need to calculate the average marks and grade for hundreds of students, and this same calculation logic is needed on the report card page, the class summary page, and the parent portal. Writing this logic separately in three places is inefficient and error-prone. How do you write this logic once and reuse it wherever needed, while also making it flexible enough to handle different numbers of subjects for different students?
Basic Function Definition and Call
A simple function that greets a user by name, demonstrating the basic structure of defining and calling a function.
Python
def greet(name): """This function greets the person passed as a parameter""" message = f"Hello, {name}! Welcome to CompileX." return message # Calling the function result = greet("Aditi") print(result)
Hello, Aditi! Welcome to CompileX.
The function 'greet' is defined with one parameter 'name'. When called with the argument "Aditi", it builds a formatted string using an f-string and returns it. The returned value is stored in 'result' and then printed.
Function with Default Parameters
Demonstrates how default parameter values allow a function to be called with fewer arguments, using the default when none is provided.
Python
def calculate_total(price, tax_rate=0.18, delivery_fee=40): total = price + (price * tax_rate) + delivery_fee return total # Using default tax_rate and delivery_fee print(calculate_total(500)) # Overriding the defaults print(calculate_total(500, 0.05, 20))
630.0 545.0
In the first call, only 'price' is provided, so Python uses the default values 0.18 for tax_rate and 40 for delivery_fee, giving 500 + 90 + 40 = 630.0. In the second call, both defaults are overridden with 0.05 and 20, giving 500 + 25 + 20 = 545.0.
Function with *args and **kwargs
Shows how to build a flexible function that accepts a variable number of positional and keyword arguments, useful for the student grading system scenario.
Python
def student_report(name, *marks, **details): average = sum(marks) / len(marks) print(f"Student: {name}") print(f"Marks: {marks}") print(f"Average: {average:.2f}") for key, value in details.items(): print(f"{key}: {value}") student_report("Rohan", 85, 90, 78, 92, section="A", roll_no=15)
Student: Rohan Marks: (85, 90, 78, 92) Average: 86.25 Delivery_fee: 40 section: A roll_no: 15
The '*marks' collects any number of positional arguments (the subject scores) into a tuple, allowing the function to handle students with different numbers of subjects. The '**details' collects any number of keyword arguments into a dictionary, allowing extra optional metadata like section and roll number to be passed flexibly.
Recursive Function Example
Demonstrates a recursive function that calculates the factorial of a number by calling itself with a smaller input until it reaches a base case.
Python
def factorial(n): if n == 0 or n == 1: return 1 else: return n * factorial(n - 1) print(factorial(5))
120
The function checks for the base case (n == 0 or n == 1) which stops the recursion. For any other value, it calls itself with (n - 1) and multiplies the result by n. So factorial(5) = 5 * 4 * 3 * 2 * 1 = 120.
  • Forgetting to Use the return Statement: Beginners often print a value inside a function instead of returning it, then try to store the function's call result in a variable. Since the function returns None by default, the variable ends up holding None instead of the expected value, causing bugs later when that variable is used in calculations or comparisons.
  • Using Mutable Default Arguments: Defining a function like 'def add_item(item, cart=[])' is a classic Python pitfall. Because default argument values are evaluated only once at function definition time, the same list object is reused across all calls that don't pass their own cart, causing items from previous unrelated calls to unexpectedly persist and accumulate.
  • Confusing Parameters with Arguments: Developers often mix up the terms 'parameter' (the variable name in the function definition) and 'argument' (the actual value passed during the function call), leading to miscommunication in code reviews and confusion when reading documentation or error messages.
  • Modifying Global Variables Without the 'global' Keyword: Trying to reassign a global variable inside a function without declaring it as 'global' creates a new local variable instead of modifying the original, leading to an UnboundLocalError or silent bugs where the outer variable appears unchanged.
  • Overusing Positional Arguments in Complex Functions: When a function has many parameters, calling it with only positional arguments makes the code hard to read and error-prone, since swapping the order of two similar-typed arguments (like swapping width and height) can cause silent logical bugs that are difficult to trace.
  • Keep Functions Small and Focused (Single Responsibility): Each function should ideally do one thing and do it well. If a function is calculating totals, sending emails, and logging data all at once, split it into three separate functions. This makes testing, debugging, and reusing code significantly easier.
  • Use Descriptive Function and Parameter Names: Name functions with verbs that describe their action, such as 'calculate_total_price' instead of 'calc' or 'func1'. Clear naming acts as self-documentation and reduces the need for excessive comments.
  • Always Add Docstrings for Non-Trivial Functions: Use triple-quoted docstrings immediately after the function definition to explain what the function does, its parameters, and its return value. This helps other developers (and tools like help() and IDEs) understand the function's purpose without reading the entire implementation.
  • Use Keyword Arguments for Clarity in Function Calls: When calling functions with multiple parameters, especially of the same data type, use keyword arguments like 'calculate_total(price=500, tax_rate=0.18)' instead of relying purely on positional order, to avoid accidental argument swapping and improve code readability.
  • Avoid Side Effects; Prefer Pure Functions When Possible: Design functions to depend only on their input parameters and return a result, without modifying external/global state whenever possible. Pure functions are easier to test, debug, and reason about, and they behave predictably regardless of when or how many times they are called.
What is the difference between a parameter and an argument in Python functions?
A parameter is the variable name listed inside the parentheses in a function's definition, acting as a placeholder for the value that will be passed in. An argument is the actual value supplied to the function when it is called. For example, in 'def greet(name):', 'name' is a parameter, but in 'greet("Aditi")', "Aditi" is the argument.
What will happen if you don't include a return statement in a Python function?
If a function does not include an explicit return statement, Python automatically returns 'None' by default. This means if you try to store the result of calling that function in a variable, the variable will hold the value None, which can lead to bugs if the caller expects an actual computed value.
Explain the difference between *args and **kwargs.
'*args' allows a function to accept any number of additional positional arguments, which are collected into a tuple inside the function. '**kwargs' allows a function to accept any number of additional keyword arguments, which are collected into a dictionary inside the function, with argument names as keys and their values as dictionary values. Both are used when the exact number of arguments a function will receive is unknown in advance.
Why is using a mutable object like a list as a default argument value considered dangerous in Python?
Default argument values in Python are evaluated only once, at the time the function is defined, not each time it is called. If a mutable object like a list or dictionary is used as a default value, that same object is shared across all calls that rely on the default, meaning modifications made in one call (like appending an item) persist and affect subsequent calls unexpectedly. The safe practice is to use 'None' as the default and initialize the mutable object inside the function body.
What is the difference between local and global scope in the context of functions, and how does the 'global' keyword affect it?
Variables defined inside a function have local scope, meaning they only exist and are accessible within that function. Variables defined outside any function have global scope and are accessible throughout the module. If you try to assign a new value to a global variable inside a function without the 'global' keyword, Python creates a new local variable with the same name instead of modifying the global one. Using 'global variable_name' inside the function explicitly tells Python to modify the variable in the global scope rather than creating a local copy.
Write a function called 'is_even' that takes a number as input and returns True if the number is even and False if it is odd.
def is_even(number): return number % 2 == 0 print(is_even(10)) # True print(is_even(7)) # False
Write a function 'find_max' that accepts any number of numeric arguments using *args and returns the largest value among them.
def find_max(*numbers): return max(numbers) print(find_max(4, 9, 2, 15, 7)) # 15
Write a recursive function 'sum_digits' that takes a positive integer and returns the sum of its digits.
def sum_digits(n): if n == 0: return 0 return n % 10 + sum_digits(n // 10) print(sum_digits(1234)) # 10

Python functions are reusable blocks of code defined using the 'def' keyword that help organize programs into logical, maintainable units. They support default parameters, variable-length arguments via *args and **kwargs, and can even call themselves recursively to solve complex problems. Mastering functions is essential for writing clean, DRY, and scalable Python code, and understanding concepts like scope, return values, and mutable default arguments is critical for both real-world development and technical interviews.

© 2026 CompileX. Maintained by Aditya Kumar Sharma.