Reusable Code with Inputs and Results
A function names a reusable operation. Its parameters describe the inputs it accepts, its body performs the work, and a return statement can provide a result to the caller.
Function Structure and Syntax
Every Python function follows the same basic structure:
def function_name(parameter_list):
"""Optional docstring to explain the function"""
# Code block to be executed
statement_1
statement_2
return return_value # OptionalFunction components consist of:
- The
defkeyword to start function definition - Function name that follows Python variable naming rules
- Parentheses containing parameter list (can be empty)
- Colon to end the definition line
- Indented code block
- Optional
returnstatement to return a value
How Function Calls Work
Calling a function evaluates the callable expression, binds the supplied arguments to parameters, and executes the function body in a new local frame.
# Function definition
def greet(name):
message = f"Hello, {name}!"
return message
# Function call
result = greet("Alice")
print(result) # Output: Hello, Alice!
# Direct call in print
print(greet("Bob")) # Output: Hello, Bob!Process that occurs during function call:
- Python evaluates the name or expression and checks that the result is callable
- Argument binding matches supplied values to the function signature
- Python executes the body in a new local scope
returnends the call with a value. Reaching the end returnsNone
Parameters and Arguments
Parameters are the variables written in a function definition, while arguments are the actual values you pass when you call that function. Python offers several kinds of parameters, and the kind a definition uses tells you whether a caller must supply that value by position, by name, or not at all.
Positional and Keyword Parameters
def introduction(name, age, city="Jakarta"):
return f"My name is {name}, {age} years old, living in {city}"
# Using positional parameters
print(introduction("Sari", 25))
# Output: My name is Sari, 25 years old, living in Jakarta
# Using keyword parameters
print(introduction(age=30, name="Budi", city="Bandung"))
# Output: My name is Budi, 30 years old, living in Bandung
# Mix of positional and keyword parameters
print(introduction("Andi", age=28, city="Surabaya"))
# Output: My name is Andi, 28 years old, living in SurabayaParameter rules:
- Positional arguments follow the parameter order
- Keyword arguments can be given in any order
- Ordinary positional arguments precede keyword arguments
- Parameters with default values are optional
Variable Number of Parameters
Python can collect extra arguments through *args and **kwargs. The function still needs to check their number, types, and values against the program's requirements.
def calculate_total(*numbers):
"""Calculate total from a number of numbers"""
total = 0
for num in numbers:
total += num
return total
# Calling with various number of arguments
print(calculate_total(1, 2, 3)) # Output: 6
print(calculate_total(5, 10, 15, 20)) # Output: 50
def student_info(name, **details):
"""Display student information with additional details"""
print(f"Name: {name}")
for key, value in details.items():
print(f"{key.capitalize()}: {value}")
# Calling with keyword arguments
student_info("Maya", age=20, major="Informatics", gpa=3.8)
# Output:
# Name: Maya
# Age: 20
# Major: Informatics
# Gpa: 3.8Parameter *args collects additional positional arguments into a tuple, while **kwargs collects additional keyword arguments into a dictionary.
Function Return Values
Functions can return values using the return statement. If there's no return or return without a value, the function will return None.
def circle_area(radius):
"""Calculate circle area"""
import math
return math.pi * radius ** 2
def find_min_max(number_list):
"""Return minimum and maximum values"""
if not number_list:
return None, None
return min(number_list), max(number_list)
def print_message(message):
"""Function without explicit return"""
print(f"Message: {message}")
# No return, automatically return None
# Usage example
area = circle_area(5)
print(f"Circle area: {area:.2f}") # Output: Circle area: 78.54
min_val, max_val = find_min_max([3, 1, 4, 1, 5, 9])
print(f"Min: {min_val}, Max: {max_val}") # Output: Min: 1, Max: 9
result = print_message("Hello World") # Output: Message: Hello World
print(f"print_message function result: {result}") # Output: print_message function result: NoneVariable Scope in Functions
Variables in Python have scope that determines where variables can be accessed. Understanding variable scope is important to avoid errors in programs.
Local and Global Variables
# Global variable
counter = 0
def add_counter():
# Local variable with same name
counter = 10
print(f"Local counter: {counter}")
def add_global_counter():
global counter
counter += 1
print(f"Global counter: {counter}")
# Usage demonstration
print(f"Initial counter: {counter}") # Output: Initial counter: 0
add_counter() # Output: Local counter: 10
print(f"Counter after function: {counter}") # Output: Counter after function: 0
add_global_counter() # Output: Global counter: 1
print(f"Final counter: {counter}") # Output: Final counter: 1Name lookup follows LEGB order:
- Local - inside current function
- Enclosing - in enclosing function (for nested functions)
- Global - at module level
- Built-in - Python built-in names
Functions as First Class Objects
In Python, functions are first-class objects, which means you can treat them like other data. You can store them in variables, pass them as arguments, or return them from other functions.
def multiply_two(x):
return x * 2
def multiply_three(x):
return x * 3
def apply_operation(function, value):
"""Apply function to value"""
return function(value)
# Store function in variable
operation = multiply_two
print(operation(5)) # Output: 10
# Store functions in list
operation_list = [multiply_two, multiply_three]
for op in operation_list:
print(op(4))
# Output:
# 8
# 12
# Pass function as argument
result1 = apply_operation(multiply_two, 7)
result2 = apply_operation(multiply_three, 7)
print(f"Results: {result1}, {result2}") # Output: Results: 14, 21Function Documentation with Docstring
A docstring is a string literal placed as the first statement in a function definition. It documents what the function does and how to use it.
def calculate_factorial(n):
"""
Calculate factorial of a nonnegative integer.
Parameters:
n (int): Nonnegative integer
Returns:
int: Factorial value of n
Raises:
ValueError: If n is negative
TypeError: If n is not an integer
Example:
>>> calculate_factorial(5)
120
>>> calculate_factorial(0)
1
"""
if not isinstance(n, int):
raise TypeError("Input must be an integer")
if n < 0:
raise ValueError("Input must be positive or zero")
if n <= 1:
return 1
return n * calculate_factorial(n - 1)
# Access docstring
print(calculate_factorial.__doc__)
# Use function
print(calculate_factorial(5)) # Output: 120
print(calculate_factorial(0)) # Output: 1Good docstring writing conventions:
- First line contains brief function summary
- If detailed explanation is needed, separate with blank line
- Explain parameters, return values, and possible exceptions
- Provide usage examples if helpful
Nested Functions and Closures
Python allows a function definition inside another function. When the returned inner function retains a free variable from an enclosing scope, the function and that retained environment form a closure.
def create_multiplier(factor):
"""Create multiplier function with specific factor"""
def multiplier(value):
"""Inner function that multiplies value by factor"""
return value * factor
return multiplier
# Create specific multiplier functions
multiply_two = create_multiplier(2)
multiply_five = create_multiplier(5)
print(multiply_two(10)) # Output: 20
print(multiply_five(4)) # Output: 20
def simple_calculator():
"""Calculator with nested functions"""
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
# Return dictionary containing functions
return {
'add': add,
'subtract': subtract,
'multiply': multiply
}
# Use calculator
calc = simple_calculator()
print(calc['add'](5, 3)) # Output: 8
print(calc['subtract'](10, 4)) # Output: 6
print(calc['multiply'](6, 7)) # Output: 42Error Handling in Functions
A function should state which inputs it accepts and fail explicitly when that contract is violated. Catch an exception only where code can recover, add useful context, or present the failure to a user.
def divide(numerator, denominator):
"""Divide two real numbers with an explicit input contract."""
if not isinstance(numerator, (int, float)):
raise TypeError("Numerator must be a real number")
if not isinstance(denominator, (int, float)):
raise TypeError("Denominator must be a real number")
if denominator == 0:
raise ValueError("Denominator must not be zero")
return numerator / denominator
def convert_to_int(value):
"""Convert a supported value or preserve Python's conversion error."""
return int(value)
# Successful calls stay direct
print(f"Division result: {divide(10, 2)}")
# Output: Division result: 5.0
print(convert_to_int("123"))
# Output: 123
# Catch at the presentation boundary where recovery is possible
for value in ("abc", [1, 2]):
try:
print(convert_to_int(value))
except (TypeError, ValueError) as error:
print(f"Could not convert {value!r}: {error}")