What is a Function in Python?¶
- A function is a block of reusable code that performs a specific task. Functions help organize code, improve modularity, and enable code reuse.
Key Features of Functions¶
- A function runs only when it is called.
- Functions allow passing parameters (input data).
- They can return a value as an output.
- Defined using the
defkeyword in Python. - Enhances code reusability and modularity.
Defining a Function in Python¶
A function in Python is defined using the
defkeyword, followed by:- A function name.
- Parentheses
()(to hold parameters, if any). - A colon (
:) to start the function block. - An optional docstring (a brief description of the function).
- An indented code block inside the function.
- A return statement (optional) to send back a result(Output of the function).
Syntax of a Function
def function_name(parameters):
"""Optional docstring"""
# Function logic here
return value # Optional return statement
Example: Function Without Parameters and Without Return Value¶
# Defining a simple function with function name welcome_message
def welcome_message(): #No parameters
"""This function prints a welcome message."""
print("Welcome to Intensity Coding")
# Calling the function
welcome_message()
#Expected Output: Welcome to Intensity Coding
Welcome to Intensity Coding
Example: Function with Parameters and Return Value¶
#Example: Function to add two numbers with function name add_numbers
def add_numbers(a, b): #Here a and b are parameters
"""Returns the sum of two numbers"""
return a + b # Returning the sum
result = add_numbers(5, 10) # Calling the function
print("Sum:", result)
#Output: Sum: 15
Sum: 15
Calling a Function in Python¶
- Once a function is defined, it can be called by using its name followed by parentheses
().
# Defining a function
def greet():
"""Prints a greeting message"""
print("Hello from Intensity Coding!")
# Calling the function
greet()
#Output: Hello from Intensity Coding!
Hello from Intensity Coding!
Example: Calling a Function with Arguments¶
# Function with parameters
def greet_user(name):
"""Greets the user with a personalized message"""
print(f"Hello, {name}! Welcome to Intensity Coding.")
# Calling function with an argument
greet_user("Alice")
# Output: Hello, Alice! Welcome to Intensity Coding.
Hello, Alice! Welcome to Intensity Coding.
Return Values in Python¶
- Functions in Python can return values using the
returnstatement. This allows us to reuse the computed value elsewhere in the program. - It helps in reusing function output , improves code modularity and makes functions more flexible.
Syntax of the return Statement
def fun():
statement_1
statement_2
statement_3
...
return [expression]
The return statement specifies the output that a function provides when it finishes execution.
- The return statement terminates the function’s execution and optionally sends a value back to the caller.
- A function does not have to include a return statement; in that case, it automatically returns None.
- If return is used without an expression, Python implicitly returns None.
- The return statement must always appear inside the function body.
def square_number(x):
"""Calculates the square of the given number and returns the result."""
return x ** 2
# Calling the function and printing the results
print(square_number(3)) # Output: 9
print(square_number(5)) # Output: 25
print(square_number(9)) # Output: 81
9 25 81
Returning Multiple Values from a Function¶
- A function can return multiple values by separating them with commas in the return statement.
- Internally, Python packs these values into a tuple, allowing you to retrieve them individually when the function is called.
Syntax
def function_name():
# statements
return value1, value2, value3
# Function that returns multiple values
def student_info():
name = "Alice"
age = 22
course = "Machine Learning"
# Returning multiple values as a tuple
return name, age, course
# Calling the function
info = student_info()
print("Returned Tuple:", info)
# Unpacking returned values
student_name, student_age, student_course = student_info()
print("Name:", student_name)
print("Age:", student_age)
print("Course:", student_course)
Returned Tuple: ('Alice', 22, 'Machine Learning')
Name: Alice
Age: 22
Course: Machine Learning
Explanation¶
- When multiple values are returned, Python groups them into a tuple by default.
- You can unpack this tuple into separate variables for easier access.
- This feature makes it convenient to return several related pieces of data from a single function.
The pass Statement¶
- Python does not allow empty function definitions. If you need to define a function without implementing it yet, you can use the
passstatement. - It helps prevents syntax errors when defining an empty function and useful for creating placeholders in large projects.
def future_function():
"""This function will be implemented later."""
pass # Placeholder to avoid errors
# Calling the function (does nothing)
future_function()
Pass by Reference vs Value in Python¶
Pass by Reference in Python¶
- In Python, all parameters (arguments) are passed by reference. This means if a function modifies a mutable object (like a list), the change will be reflected outside the function as well.
# Example: Pass by Reference (Mutable Object)
# Function modifies the list by appending elements
def changeme(mylist):
"""Appends new elements to the list"""
mylist.append([1, 2, 3, 4])
print("Values inside the function:", mylist)
# Creating a list
mylist = [10, 20, 30]
# Calling the function
changeme(mylist)
# Checking the list after function call
print("Values outside the function:", mylist)
# Output:
# Values inside the function: [10, 20, 30, [1, 2, 3, 4]]
# Values outside the function: [10, 20, 30, [1, 2, 3, 4]]
Values inside the function: [10, 20, 30, [1, 2, 3, 4]] Values outside the function: [10, 20, 30, [1, 2, 3, 4]]
Example: Overwriting the Reference (Pass by Value Behavior)¶
- When a new object is assigned to a parameter inside the function, the original reference is lost, and changes do not reflect outside.
# Function assigns a new list, breaking the reference
def changeme(mylist):
"""Assigns a new list inside the function"""
mylist = [1, 2, 3, 4] # Assigns a new reference
print("Values inside the function:", mylist)
# Creating a list
mylist = [10, 20, 30]
# Calling the function
changeme(mylist)
# Checking the list after function call
print("Values outside the function:", mylist)
# Output:
# Values inside the function: [1, 2, 3, 4]
# Values outside the function: [10, 20, 30]
Values inside the function: [1, 2, 3, 4] Values outside the function: [10, 20, 30]
Function Arguments in Python¶
- Functions can accept arguments (parameters), which are values passed during a function call.
Parameters vs Arguments in Python¶
The terms parameters and arguments are often used interchangeably, but they have distinct meanings:
Parameter: A variable listed inside the parentheses in the function definition.
Argument: The actual value passed to a function when calling it. It refers to the input data on which a function operates to perform a specific action and produce an output result.
# Function with parameter 'name'
def greet_user(name):
"""Greets the user with a personalized message"""
print(f"Hello, {name}! Welcome to Intensity Coding.")
# Calling function with an argument 'Alice'
greet_user("Alice")
# Output: Hello, Alice! Welcome to Intensity Coding.
Hello, Alice! Welcome to Intensity Coding.
Number of Arguments in Python Functions¶
- By default, a function must be called with the correct number of arguments. If the function expects two arguments, it must be called with exactly two.
# Example: Function with Multiple Arguments
# Function with two parameters
def my_function(fname, lname):
"""Prints full name"""
print(fname + " " + lname)
# Calling function with two arguments
my_function("Emil", "Refsnes")
# Output: Emil Refsnes
Emil Refsnes
# Example: Missing an Argument (Causes Error)
# Function expecting two parameters
def my_function(fname, lname):
"""Prints full name"""
print(fname + " " + lname)
# Uncommenting below will raise an error
# my_function("Emil") # TypeError: my_function() missing 1 required positional argument: 'lname'
Required Arguments¶
- A required argument must be passed when calling a function; otherwise, Python raises an error.
# Function that prints a given string
def printme(string):
"""Prints the given string"""
print(string)
# Calling function with required argument
printme("Hello, Intensity Coding!")
# Output: Hello, Intensity Coding!
# Uncommenting the below line will cause an error
# printme() # TypeError: printme() missing 1 required argument
Hello, Intensity Coding!
Types of Function Arguments:¶
- Positional Arguments
- Keyword Arguments
- Default Arguments
- Variable-Length Arguments
Positional Arguments¶
- Positional arguments are the most common and straightforward way to pass data into a function. They are called positional because the position of each argument in the function call determines which parameter it corresponds to.
- When you call function, the arguments you provide are assigned to parameters based on their position.
Rules for Using Positional Arguments
- Order matters: The first argument corresponds to the first parameter, the second to the second, and so on.
- All required positional arguments must be provided: If any are missing, Python raises a TypeError.
- They cannot come after keyword arguments in a function call.
# Function definition
def greet(name, age):
print(f"Hello {name}, you are {age} years old.")
# Function call with positional arguments
greet("Alice", 25)
#Output: Hello Alice, you are 25 years old.
# If you change the order of arguments, the output will also change:
greet(25, "Alice")
#Output: Hello 25, you are Alice years old.
Hello Alice, you are 25 years old. Hello 25, you are Alice years old.
Keyword Arguments¶
- Python allows passing arguments using key-value pairs (
key=value). - The order of arguments does not matter when using keyword arguments but the number of arguments must match. Otherwise, we will get an error.
- All the arguments after the first keyword argument must also be keyword arguments too.
# Function to configure an AI model using keyword arguments
def configure_model(optimizer, loss_function, activation):
"""Prints AI model settings using keyword arguments"""
print("Model Configuration:")
print(f"Optimizer: {optimizer}")
print(f"Loss Function: {loss_function}")
print(f"Activation Function: {activation}")
# Calling function with keyword arguments in different orders
configure_model(optimizer="Adam", loss_function="MSE", activation="ReLU")
# Output:
# Model Configuration:
# Optimizer: Adam
# Loss Function: MSE
# Activation Function: ReLU
print("----------")
configure_model(loss_function="CrossEntropy", activation="Sigmoid", optimizer="SGD")
# Output:
# Model Configuration:
# Optimizer: SGD
# Loss Function: CrossEntropy
# Activation Function: Sigmoid
Model Configuration: Optimizer: Adam Loss Function: MSE Activation Function: ReLU ---------- Model Configuration: Optimizer: SGD Loss Function: CrossEntropy Activation Function: Sigmoid
- Note: Keyword arguments can only be used after all positional arguments.
def display_info(name, age):
print(f"Name: {name}, Age: {age}")
# Correct usage
display_info("Raj", 25) # Positional only
display_info("Meet", age=30) # Positional + Keyword
# Incorrect usage
# display_info(name="Charlie", 35) # Positional argument after keyword argument not allowed
Name: Raj, Age: 25 Name: Meet, Age: 30
Default Parameter Value¶
- A default parameter is used when an argument is not provided.
# Function with default training settings
def train_model(learning_rate=0.01, batch_size=32, epochs=10):
"""Prints training settings with default values"""
print(f"Training with learning_rate={learning_rate}, batch_size={batch_size}, epochs={epochs}")
# Calling function with custom values
train_model(0.001, 64, 20)
# Output: Training with learning_rate=0.001, batch_size=64, epochs=20
# Calling function without arguments (uses default values)
train_model()
# Output: Training with learning_rate=0.01, batch_size=32, epochs=10
Training with learning_rate=0.001, batch_size=64, epochs=20 Training with learning_rate=0.01, batch_size=32, epochs=10
- Note: Default parameters must always come after non-default parameters.
# Correct: Non-default arguments come first
def greet(name, message="Hello"):
print(f"{message}, {name}!")
greet("Raj") # Output: Hello, Raj!
greet("Meet", "Hi") # Output: Hi, Meet!
# Incorrect: Default argument before non-default argument
# def greet(message="Hello", name): # This will raise a SyntaxError
# print(f"{message}, {name}!")
Hello, Raj! Hi, Meet!
Variable-Length Arguments¶
Arbitrary Arguments (*args)¶
- If you don’t know how many arguments will be passed into a function, use
*args. - The function receives arguments as a tuple and can access them using indexing.
- The *args argument exhausts positional arguments so you can only use keyword arguments after it.
# Function to print multiple AI models
def list_models(*models):
"""Prints a list of AI models provided as arguments"""
print("Available AI Models:")
for model in models:
print(model)
# Calling function with multiple model names
list_models("Decision Tree", "Neural Network", "Random Forest")
# Output:
# Available AI Models:
# Decision Tree
# Neural Network
# Random Forest
Available AI Models: Decision Tree Neural Network Random Forest
# Function with Required and Arbitrary Arguments
# Function to print a required dataset name and optional features
def dataset_info(dataset_name, *features):
"""Prints dataset name and additional feature details"""
print("Dataset Name:", dataset_name)
print("Features:")
for feature in features:
print(feature)
# Calling function with different numbers of features
dataset_info("Image Dataset")
# Output:
# Dataset Name: Image Dataset
# Features:
print("----------")
dataset_info("Image Dataset", "Grayscale", "Size: 28x28 pixels")
# Output:
# Dataset Name: Image Dataset
# Features:
# Grayscale
# Size: 28x28 pixels
Dataset Name: Image Dataset Features: ---------- Dataset Name: Image Dataset Features: Grayscale Size: 28x28 pixels
# Enforcing Keyword-Only Arguments After *args
# Function that accepts variable positional arguments and a keyword-only argument
def log_data(*args, log_level="INFO"):
# Print all positional arguments
print("Data:", args)
# Print the log level
print("Log Level:", log_level)
# Valid usage: 'log_level' passed as a keyword argument
log_data(10, 20, 30, log_level="DEBUG")
# Invalid usage: passing 'log_level' as positional will raise an error
# log_data(10, 20, 30, "DEBUG") # TypeError
Data: (10, 20, 30) Log Level: DEBUG
Arbitrary Keyword Arguments(**kwargs)¶
- If you don’t know how many keyword arguments will be passed, use
**kwargs. - The function receives arguments as a dictionary.
- Always place the **kwargs parameter at the end of the parameter list, or you will get an error.
# Function to print dataset details
def dataset_details(**info):
"""Prints dataset details dynamically"""
print("Dataset Details:")
for key, value in info.items():
print(f"{key}: {value}")
# Calling function with dataset details
dataset_details(name="MNIST", samples=60000, image_size="28x28 pixels")
# Output:
# Dataset Details:
# name: MNIST
# samples: 60000
# image_size: 28x28 pixels
Dataset Details: name: MNIST samples: 60000 image_size: 28x28 pixels
Positional-Only and Keyword-Only Arguments¶
- Python allows specifying whether function arguments must be passed positionally or as keyword arguments.
Positional-Only Arguments¶
- To enforce positional-only arguments, add
/after them.
def multiply(x, /):
"""Multiplies a given number by 2. Requires a positional argument."""
print(x * 2)
# Correct usage (positional argument)
multiply(5) # Output: 10
# Incorrect usage (keyword argument, raises TypeError)
# multiply(x=5)
10
Keyword-Only Arguments¶
- To enforce keyword-only arguments, add
*before them.
def multiply(*, x):
"""Multiplies a given number by 2. Requires a keyword argument."""
print(x * 2)
# Correct usage (keyword argument)
multiply(x=5) # Output: 10
# Incorrect usage (positional argument, raises TypeError)
# multiply(5)
10
Combining Positional-Only and Keyword-Only¶
- You can define functions that require both positional-only and keyword-only arguments.
def compute(a, b, /, *, c, d):
"""Computes a calculation using mixed argument types."""
print((a + b) * (c - d))
# Correct usage
compute(5, 6, c=8, d=3) # Output: 55
# Incorrect usages (raises TypeError)
# compute(a=5, b=6, c=8, d=3)
# compute(5, 6, 8, 3)
# compute(c=8, d=3, 5, 6)
55
Recursion in Python¶
- Recursion is when a function calls itself. It is commonly used in problems involving mathematical calculations (factorial, Fibonacci series) and Tree and graph traversals.
Key Considerations When Using Recursion¶
- Every recursive function must have a base case to avoid infinite recursion.
- Recursive functions can consume a lot of memory if not handled correctly.
def recursive_sum(n):
"""
Recursively calculates the sum of all numbers from n to 0.
"""
if n > 0:
result = n + recursive_sum(n - 1) # Recursive call
print(f"Summing: {n}, Current Sum: {result}")
else:
result = 0 # Base case
return result
# Calling the recursive function
print("\nRecursion Example Results:")
recursive_sum(6)
#Output:
# Summing: 1, Current Sum: 1
# Summing: 2, Current Sum: 3
# Summing: 3, Current Sum: 6
# Summing: 4, Current Sum: 10
# Summing: 5, Current Sum: 15
# Summing: 6, Current Sum: 21
# 21
Recursion Example Results: Summing: 1, Current Sum: 1 Summing: 2, Current Sum: 3 Summing: 3, Current Sum: 6 Summing: 4, Current Sum: 10 Summing: 5, Current Sum: 15 Summing: 6, Current Sum: 21
21
Python Closures and Nested Functions¶
Nested Functions in Python¶
- In Python, you can define a function inside another function.
- Such an inner function is called a nested function.
- Nested functions become powerful when they retain access to variables from their enclosing function even after the outer function has finished execution. This concept is known as a closure.
#Basic Nested Function
def greet_user():
# Outer function variable
message = "Welcome to Intensity Coding!"
# Nested (inner) function
def display_message():
# Accessing variable from outer function
print(message)
# Calling the inner function
display_message()
# Execute the outer function
greet_user()
Welcome to Intensity Coding!
Explanation:
display_message()is defined insidegreet_user().- The variable message is defined in the outer function but is accessible inside the inner function.
- This inner function is called a nested function.
Free Variables and Closures¶
- When a nested function remembers variables from its outer function, even after the outer function has finished running, Python forms a closure.
- A closure is a function that captures variables from its enclosing scope so they remain available for later use.
# Returning a Function (Closure)
def outer_function():
greeting = "Hello from Intensity Coding!"
# Inner function that uses outer variable
def inner_function():
print(greeting)
# Return the inner function instead of calling it
return inner_function
# Get the closure
closure_fn = outer_function()
# Call the returned function
closure_fn()
Hello from Intensity Coding!
Key Points:
- The outer_function() completes execution but still,
closure_fn()accesses the variablegreeting. - The variable
greetingis called a free variable because it’s not defined in inner_function() but is still available. - Python stores this reference in a cell object to preserve it for later use.
Inspecting Closures in Python¶
- Python internally stores free variables in an object called a cell.
- You can inspect this using the closure attribute.
def create_closure():
text = "Closure Example in Intensity Coding"
def inner():
print(text)
return inner
fn = create_closure()
# Inspect the closure cells
print(fn.__closure__)
print("Free variables:", fn.__code__.co_freevars)
(<cell at 0x7afa7f1a58a0: str object at 0x7afa7f160800>,)
Free variables: ('text',)
Explanation:
- The closure attribute shows a tuple of cell objects storing the captured variables.
fn.__code__.co_freevarslists the names of these variables.
Memory Behavior of Closures¶
- Closures keep references to variables, not copies.
- Hence, both the outer and inner functions point to the same memory address.
def show_memory():
msg = "Hello Intensity Coding"
print("Outer variable memory address:", hex(id(msg)))
def inner():
print("Inner variable memory address:", hex(id(msg)))
print(msg)
return inner
fn = show_memory()
fn()
Outer variable memory address: 0x7afa7f1aea70 Inner variable memory address: 0x7afa7f1aea70 Hello Intensity Coding
Nonlocal Variables in Nested Functions¶
- When working with nested functions, variables declared in the outer (enclosing) function are neither local nor global.
- To modify them inside an inner function, Python provides the nonlocal keyword.
# Example: Demonstrating nonlocal variable
def outer_function():
message = "Outer"
def inner_function():
nonlocal message # Refers to the variable in the outer function
message = "Modified by Inner Function"
print("Inside Inner:", message)
inner_function()
print("Inside Outer:", message)
outer_function()
# Output:
# Inside Inner: Modified by Inner Function
# Inside Outer: Modified by Inner Function
Inside Inner: Modified by Inner Function Inside Outer: Modified by Inner Function
Explanation:
- The nonlocal keyword allows the inner function to modify a variable in its enclosing (outer) scope.
Variable Scopes¶
- In Python, when you assign an object to a variable, that variable becomes a reference (or label) to that object in memory. However, where you define that variable determines where you can access it—this is known as the variable's scope.
Types of Scopes in Python¶
Python uses lexical scoping, which means the position of your variable definitions in the source code determines their visibility.
Each scope has its own namespace, essentially a mapping between variable names and their associated objects.
There are four types of scopes:
| Scope Type | Description |
|---|---|
| Built-in Scope | Contains Python’s built-in functions and constants like print(), len() |
| Global Scope | Exists at the module level—visible throughout the module |
| Enclosing Scope | Used in nested functions (nonlocal scope) |
| Local Scope | Exists within functions—temporary and specific to the function call |
Python’s Variable Lookup Mechanism (LEGB Rule)¶
When you reference a variable, Python searches through the scopes in this order:
- Local – current function
- Enclosing – enclosing functions (if any)
- Global – module-level
- Built-in – built-in names like
print,len, etc.
1. Local Scope¶
- A local scope is created whenever a function is executed.
- Any variable defined inside a function belongs to that function’s local namespace and exists only for the duration of that call.
def func_a():
my_var = "Inside func_a" # Local scope for func_a
print(my_var)
func_a()
# Expected Output:
# Inside func_a
Inside func_a
- Trying to access my_var outside func_a() results in an error because the variable is not visible globally:
def func_a():
my_var = "Inside func_a"
func_a()
print(my_var) # Error: my_var is not defined
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /tmp/ipython-input-3931203131.py in <cell line: 0>() 3 4 func_a() ----> 5 print(my_var) # Error: my_var is not defined NameError: name 'my_var' is not defined
- Local scopes are independent across different functions:
def func_a():
a = "Value from A"
print(a)
def func_b():
b = "Value from B"
print(b)
func_a()
func_b()
Value from A Value from B
- If the same variable name appears in multiple functions, each function works with its own local copy:
def func_a():
my_var = "Local A"
print(my_var)
def func_b():
my_var = "Local B"
print(my_var)
func_a() # Prints "Local A"
func_b() # Prints "Local B"
Local A Local B
- Important Note: A new local scope is created every time the function is called — including recursive calls.
- Python does not create local scopes at definition time; they only exist during execution.
2. Enclosing Scope¶
- An enclosing scope appears when functions are nested.
- The inner function can “see” variables from the outer (enclosing) function’s scope, provided they are not overridden locally.
def outer_function():
message = "From outer scope"
def inner_function():
print(message) # message is not in local scope, so Python checks enclosing scope
inner_function()
outer_function()
# Expected Output:
# From outer scope
From outer scope
Here, message is not inside the inner function’s local namespace, so Python moves outward and finds it in the enclosing function.
If the enclosing function did not have message, Python would continue searching following the LEGB rule.
3. Global Scope¶
- A global scope contains variables defined at the top level of a script or module. These variables are visible everywhere, unless shadowed by local or enclosing names.
my_var = 10 # Global variable
def func_a():
print(my_var) # No local or enclosing my_var → global is used
def func_b():
print(my_var)
func_a()
func_b()
# Expected Output:
# 10
# 10
10 10
- Both functions rely on the global variable because neither declared a local version.
4. Built-in Scope¶
- Python loads a collection of built-in names every time you start the interpreter or run a script.
- These names include functions, types, and exceptions that are always available.
- You can inspect them:
print(len(dir(__builtins__))) # Number of built-in names
160
- Common built-ins include:
len, range, int, float, set, dict, zip, str, list
- If a name cannot be found in the local, enclosing, or global scopes, Python finally checks this built-in namespace.