What Is It?
What Are Functions in Python?
A function is a reusable block of code that performs a specific task. You define it once and call it as many times as you need, optionally passing data in (parameters) and getting data back (return values).
Functions are defined using the def keyword:
def greet(name):
return f"Hello, {name}!"
message = greet("Aarav")
print(message) # Hello, Aarav!The function greet takes one parameter (name), constructs a greeting string, and returns it to the caller. The caller stores the result in message and prints it.
Function Anatomy
def function_name(parameter1, parameter2): # Function signature
"""Docstring: describes what the function does.""" # Optional
# Function body (indented)
result = parameter1 + parameter2
return result # Return value (optional)
# Calling the function
output = function_name(10, 20) # Arguments passed to parametersKey terminology: Parameters are the variable names in the function definition. Arguments are the actual values you pass when calling the function.
Why Does It Matter?
Why Are Functions Important?
Functions are arguably the most important concept in programming. Every significant program uses them. Here is why:
1. DRY Principle (Don't Repeat Yourself)
Without functions, you would copy-paste the same code every time you need it. If Aarav writes a formula to calculate the area of a circle in 5 different places, and the formula needs updating, he must change all 5 places. With a function, he changes it once.
2. Modularity and Organization
Functions break large problems into smaller, manageable pieces. Instead of a 500-line script, you have 20 functions of 25 lines each. Each function has a clear name and purpose, making the code easier to understand and navigate.
3. Reusability
Once written, a function can be called from anywhere in your program, or even imported into other programs. Libraries like math, random, and os are collections of functions that thousands of developers reuse.
4. Testing and Debugging
Functions can be tested independently. If calculate_discount() gives the wrong result, you know exactly where the bug is. Without functions, finding a bug in 500 lines of sequential code is much harder.
5. Abstraction
Functions hide complexity. When Priya calls sorted(my_list), she does not need to know the sorting algorithm. She just needs to know what the function does, what it accepts, and what it returns. This is abstraction: separating the what from the how.
Detailed Explanation
Detailed Explanation
1. The def Keyword and Basic Functions
Every function starts with def, followed by the function name, parentheses (with optional parameters), and a colon. The body is indented:
def say_hello():
print("Hello!")
say_hello() # Prints: Hello!
say_hello() # Can call as many times as you wantA function with no parameters and no return value simply executes its body when called.
2. Parameters and Arguments
Parameters (defined in the function signature) receive values from arguments (passed when calling):
def add(a, b): # a and b are parameters
return a + b
result = add(3, 5) # 3 and 5 are arguments
print(result) # 83. The return Statement
return sends a value back to the caller and immediately exits the function. Any code after return is not executed.
def square(n):
return n ** 2
print("This never runs") # Dead code
result = square(5) # 25Returning Multiple Values
Python functions can return multiple values as a tuple:
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 1, 7, 2, 9])
print(low, high) # 1 9Returning None
If a function has no return statement, or uses return without a value, it returns None:
def greet(name):
print(f"Hello, {name}")
result = greet("Aarav") # Prints: Hello, Aarav
print(result) # None4. Default Parameters
Parameters can have default values. If the caller does not provide an argument, the default is used:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Aarav")) # Hello, Aarav!
print(greet("Priya", "Welcome")) # Welcome, Priya!Important rule: default parameters must come after non-default parameters. def f(a=1, b) is a SyntaxError.
5. Keyword Arguments vs Positional Arguments
Positional arguments are matched by position. Keyword arguments are matched by name:
def describe(name, age, city):
print(f"{name}, age {age}, from {city}")
# Positional
describe("Aarav", 16, "Delhi")
# Keyword (order does not matter)
describe(city="Mumbai", name="Priya", age=15)
# Mixed (positional must come first)
describe("Rohan", city="Pune", age=17)6. *args: Variable Positional Arguments
*args collects extra positional arguments into a tuple:
def total(*args):
print(type(args)) # <class 'tuple'>
return sum(args)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100The name args is a convention. You could use *numbers or *values. The * is what matters.
7. **kwargs: Variable Keyword Arguments
**kwargs collects extra keyword arguments into a dictionary:
def profile(**kwargs):
print(type(kwargs)) # <class 'dict'>
for key, value in kwargs.items():
print(f"{key}: {value}")
profile(name="Aarav", age=16, city="Delhi")
# name: Aarav
# age: 16
# city: Delhi8. Combining Parameter Types
The correct order is: positional, *args, keyword, **kwargs:
def example(a, b, *args, key="default", **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"key={key}")
print(f"kwargs={kwargs}")
example(1, 2, 3, 4, key="custom", x=10, y=20)
# a=1, b=2
# args=(3, 4)
# key=custom
# kwargs={'x': 10, 'y': 20}9. Docstrings
A docstring is a triple-quoted string as the first statement of a function. It documents what the function does, its parameters, and its return value:
def calculate_area(radius):
"""Calculate the area of a circle.
Args:
radius: The radius of the circle (non-negative number).
Returns:
The area of the circle as a float.
"""
import math
return math.pi * radius ** 2
# Access the docstring
print(calculate_area.__doc__)
help(calculate_area)10. Functions as First-Class Objects
In Python, functions are objects. You can assign them to variables, pass them as arguments, and store them in data structures:
# Assign function to a variable
def square(x):
return x ** 2
operation = square
print(operation(5)) # 25
# Pass function as argument
def apply(func, value):
return func(value)
print(apply(square, 7)) # 49
print(apply(len, "hello")) # 511. Recursion
A recursive function calls itself. Every recursive function needs a base case (when to stop) and a recursive case (when to call itself again):
# Factorial: n! = n * (n-1)!
def factorial(n):
if n == 0 or n == 1: # Base case
return 1
return n * factorial(n - 1) # Recursive case
print(factorial(5)) # 120 (5 * 4 * 3 * 2 * 1)Recursion is elegant for problems that have a naturally recursive structure (factorial, Fibonacci, tree traversal), but can be slower and use more memory than iterative solutions for large inputs.
12. Nested Functions
Functions can be defined inside other functions. The inner function has access to the outer function's variables:
def outer(message):
def inner():
print(message) # Accesses outer's variable
inner()
outer("Hello from inside!") # Hello from inside!Nested functions are the foundation of closures, which we will explore in the next chapter.
13. Global vs Local Variables (Preview)
Variables defined inside a function are local: they exist only during the function call. Variables defined outside all functions are global:
x = 10 # Global
def example():
y = 20 # Local
print(x) # Can read global
print(y) # Can read local
example()
print(x) # 10 (global is accessible)
# print(y) # NameError! y does not exist outside the functionThe full rules of scope (LEGB rule) are covered in the next chapter on Scope and Closures.
Code Examples
# Function with no parameters, no return
def say_hello():
print("Hello, World!")
say_hello()
say_hello()
# Function with parameters and return
def add(a, b):
return a + b
result = add(10, 20)
print(f"Sum: {result}")
# Return value of a function without return
def just_print(msg):
print(msg)
value = just_print("Testing")
print(f"Return value: {value}")def. say_hello() has no parameters and no return value. add(a, b) takes two parameters and returns their sum. A function without a return statement implicitly returns None.def create_profile(name, age, city="Unknown", active=True):
return {
"name": name,
"age": age,
"city": city,
"active": active
}
# All positional
p1 = create_profile("Aarav", 16, "Delhi", True)
print(p1)
# Using defaults
p2 = create_profile("Priya", 15)
print(p2)
# Keyword arguments (order does not matter)
p3 = create_profile(age=17, name="Rohan", city="Mumbai")
print(p3)
# Mix of positional and keyword
p4 = create_profile("Meera", 14, active=False)
print(p4)city="Unknown") are used when the caller does not provide a value. Keyword arguments (age=17) can be passed in any order. When mixing, positional arguments must come before keyword arguments.def analyze_marks(marks):
"""Analyze a list of marks and return statistics."""
total = sum(marks)
average = total / len(marks)
highest = max(marks)
lowest = min(marks)
return total, average, highest, lowest
marks = [85, 92, 78, 95, 88]
t, avg, hi, lo = analyze_marks(marks)
print(f"Total: {t}")
print(f"Average: {avg}")
print(f"Highest: {hi}")
print(f"Lowest: {lo}")
# The return is actually a tuple
result = analyze_marks(marks)
print(f"\nAs tuple: {result}")
print(f"Type: {type(result)}")t, avg, hi, lo = ...). Without unpacking, the result is a single tuple.# *args: variable number of positional arguments
def total(*args):
print(f"args = {args} (type: {type(args).__name__})")
return sum(args)
print(total(1, 2, 3))
print(total(10, 20, 30, 40, 50))
# **kwargs: variable number of keyword arguments
def display_info(**kwargs):
print(f"kwargs = {kwargs} (type: {type(kwargs).__name__})")
for key, value in kwargs.items():
print(f" {key}: {value}")
print()
display_info(name="Aarav", age=16, grade="A")
# Combining regular params, *args, and **kwargs
def flexible(required, *args, **kwargs):
print(f"required: {required}")
print(f"args: {args}")
print(f"kwargs: {kwargs}")
print()
flexible("must", 1, 2, 3, x=10, y=20)*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. They can be combined: regular parameters first, then *args, then **kwargs.def square(x):
return x ** 2
def cube(x):
return x ** 3
# Assign function to a variable
op = square
print(op(5)) # 25
# Store functions in a list
operations = [square, cube, abs, len]
for func in operations:
if func in (len,):
print(f"{func.__name__}('hello') = {func('hello')}")
else:
print(f"{func.__name__}(-3) = {func(-3)}")
# Pass function as argument
def apply_twice(func, value):
return func(func(value))
print(f"\nsquare applied twice to 3: {apply_twice(square, 3)}")
print(f"cube applied twice to 2: {apply_twice(cube, 2)}")func.__name__ gives the function's name as a string.# Factorial: n! = n * (n-1) * ... * 1
def factorial(n):
if n <= 1: # Base case
return 1
return n * factorial(n - 1) # Recursive case
print(f"5! = {factorial(5)}")
print(f"0! = {factorial(0)}")
print(f"10! = {factorial(10)}")
# Fibonacci: F(n) = F(n-1) + F(n-2)
def fibonacci(n):
if n <= 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
print(f"\nFirst 10 Fibonacci numbers:")
for i in range(10):
print(fibonacci(i), end=" ")
print()
# Recursive sum of digits
def digit_sum(n):
if n < 10:
return n
return n % 10 + digit_sum(n // 10)
print(f"\nDigit sum of 12345: {digit_sum(12345)}")def calculate_bmi(weight_kg, height_m):
"""Calculate Body Mass Index (BMI).
Args:
weight_kg: Weight in kilograms (positive number).
height_m: Height in meters (positive number).
Returns:
A tuple of (bmi_value, category) where category is
one of: 'Underweight', 'Normal', 'Overweight', 'Obese'.
"""
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
return round(bmi, 1), category
# Using the function
bmi, cat = calculate_bmi(70, 1.75)
print(f"BMI: {bmi}, Category: {cat}")
# Accessing docstring
print(f"\nDocstring:\n{calculate_bmi.__doc__}")func.__doc__ or help(func). Well-documented functions are easier to use and maintain.def outer(x):
def inner(y):
return x + y # inner can access outer's variable x
return inner(10)
result = outer(5)
print(f"outer(5) -> inner(10) = {result}")
# Global vs Local
counter = 0 # Global
def increment():
counter = 1 # Local! Does NOT modify global
print(f"Inside function: counter = {counter}")
increment()
print(f"Outside function: counter = {counter}")
# Demonstrating local scope
def demo():
local_var = "I exist only inside demo"
print(local_var)
demo()
try:
print(local_var)
except NameError as e:
print(f"NameError: {e}")Common Mistakes
Forgetting to Call the Function (Missing Parentheses)
def greet():
return "Hello!"
result = greet # Missing ()
print(result) # <function greet at 0x...> - prints the function object!def greet():
return "Hello!"
result = greet() # Call with ()
print(result) # Hello!() to call a function: greet(), not greet.Using print() Instead of return (Function Returns None)
def add(a, b):
print(a + b) # Prints but does not return
result = add(3, 5) # Prints 8
print(result * 2) # TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'def add(a, b):
return a + b # Return the value
result = add(3, 5)
print(result * 2) # 16print() displays a value to the screen but does not send it back to the caller. The function returns None. Use return when you need the caller to use the computed value.Mutable Default Parameter (Shared Between Calls)
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] - not ['banana']!def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana']None as the default and create the mutable object inside the function.Placing Default Parameters Before Non-Default
def greet(greeting="Hello", name):
return f"{greeting}, {name}!"
# SyntaxError: non-default argument follows default argumentdef greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Aarav")) # Hello, Aarav!
print(greet("Priya", "Welcome")) # Welcome, Priya!Modifying a Global Variable Without the global Keyword
count = 0
def increment():
count += 1 # UnboundLocalError!
increment()count = 0
def increment():
global count
count += 1
increment()
print(count) # 1count += 1 reads and writes, Python sees it as local, but it has not been defined locally yet. Use global count to modify the global variable.Recursion Without a Base Case (Infinite Recursion)
def countdown(n):
print(n)
countdown(n - 1) # No base case!
countdown(5) # RecursionError: maximum recursion depth exceededdef countdown(n):
if n <= 0: # Base case
print("Done!")
return
print(n)
countdown(n - 1)
countdown(5)Summary
- Functions are defined with the def keyword. They encapsulate reusable logic, improving code organization and reducing repetition (DRY principle).
- Parameters are defined in the function signature. Arguments are the values passed when calling. Parameters vs arguments is a common interview question.
- The return statement sends a value back to the caller and exits the function. Without return, a function returns None implicitly.
- Python functions can return multiple values as a tuple: return a, b, c. The caller can unpack them: x, y, z = func().
- Default parameters provide fallback values: def f(x, y=10). Non-default parameters must come before default ones.
- Keyword arguments can be passed in any order: f(y=2, x=1). Positional arguments must come before keyword arguments.
- *args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary.
- Functions are first-class objects in Python. They can be assigned to variables, stored in data structures, and passed as arguments to other functions.
- Recursive functions call themselves with a smaller input. Every recursive function needs a base case (stopping condition) to prevent infinite recursion.
- Docstrings (triple-quoted strings) document what a function does, its parameters, and return value. Access with func.__doc__ or help(func).
- Variables inside a function are local (exist only during the call). Variables outside all functions are global. Use the global keyword to modify a global from inside a function.