Skip to content

Python 3 - Functions

Functions are named blocks of reusable code designed to perform a specific task. They are essential for structuring applications, improving code organization, and promoting reusability (following the DRY principle - Don’t Repeat Yourself).

Python provides many built-in functions (like print(), len(), type()), but you can also define your own, known as user-defined functions.

Key rules for defining a function in Python:

  • Start with the keyword def followed by the function name and parentheses (). Function names follow the same rules as variable names (letters, numbers, underscore; cannot start with a number). Conventionally, function names use snake_case.
  • Input parameters (arguments) the function accepts are placed inside the parentheses, separated by commas.
  • The function header line ends with a colon :.
  • The indented block of code following the colon is the function’s body (suite).
  • Optional: The first statement in the body can be a string literal called a docstring (documentation string), enclosed in triple quotes """Docstring goes here""". It describes the function’s purpose.
  • Use the return statement to exit the function and optionally send back a value (or values) to the caller. A return without an expression returns None. If there’s no return statement, the function implicitly returns None upon reaching the end of its body.
def function_name(parameter1, parameter2, ...):
"""Optional docstring explaining the function."""
# Function body (indented code)
statement1
statement2
...
return [value] # Optional return statement

Type Hinting (Optional but Recommended): Modern Python encourages adding type hints for parameters and the return value for better code clarity and tooling support (like static analysis). They don’t affect runtime behavior by default.

def greet(name: str) -> None:
"""Greets the user by name.
Args:
name: The name of the person to greet.
"""
print(f"Hello, {name}!")
def add(a: int, b: int) -> int:
"""Returns the sum of two integers."""
return a + b
# Defining the function
def print_message(message: str):
"""Prints the provided message to the console."""
print(message)
# Implicitly returns None

Defining a function just creates it. To execute the code inside, you must call the function by using its name followed by parentheses, providing any required arguments.

#!/usr/bin/env python3
# Function definition
def print_message(message: str):
"""Prints the provided message to the console."""
print(message)
# --- Calling the function ---
print_message("This is the first call!")
print_message("This is the second call.")
result = print_message("Calling and assigning result...") # Function executes
print(f"The function returned: {result}") # Output: None

Output:

This is the first call!
This is the second call.
Calling and assigning result...
The function returned: None

Argument Passing: Pass by Object Reference

Section titled “Argument Passing: Pass by Object Reference”

Python’s argument passing mechanism is often described as “pass by object reference” or “pass by assignment”. What this means:

  • When you pass an object (like a number, string, list) to a function, the function parameter becomes a new reference (name) pointing to that same object.
  • If the object passed is mutable (e.g., a list, dictionary), modifications made to the object’s contents inside the function will affect the original object outside the function.
  • If you reassign the parameter variable inside the function to a new object, this only changes the local reference within the function; it does not affect the original variable outside the function.

Example 1: Modifying a mutable object (list)

#!/usr/bin/env python3
def modify_list(some_list: list):
"""Appends an item to the passed list."""
print(f"Inside function (before change): {some_list}, ID: {id(some_list)}")
some_list.append(100) # Modifies the original list object
print(f"Inside function (after change): {some_list}, ID: {id(some_list)}")
my_list = [10, 20, 30]
print(f"Outside function (before call): {my_list}, ID: {id(my_list)}")
modify_list(my_list)
print(f"Outside function (after call): {my_list}, ID: {id(my_list)}")

Output (IDs will vary but show the same list object being used):

Outside function (before call): [10, 20, 30], ID: <some_id>
Inside function (before change): [10, 20, 30], ID: <some_id>
Inside function (after change): [10, 20, 30, 100], ID: <some_id>
Outside function (after call): [10, 20, 30, 100], ID: <some_id>

Example 2: Reassigning the parameter

#!/usr/bin/env python3
def reassign_list(some_list: list):
"""Reassigns the parameter to a new list."""
print(f"Inside function (before reassign): {some_list}, ID: {id(some_list)}")
some_list = [1, 2, 3, 4] # Creates a NEW local list
print(f"Inside function (after reassign): {some_list}, ID: {id(some_list)}")
my_list = [10, 20, 30]
print(f"Outside function (before call): {my_list}, ID: {id(my_list)}")
reassign_list(my_list)
print(f"Outside function (after call): {my_list}, ID: {id(my_list)}") # Original is unchanged

Output (IDs will vary but show the parameter getting a new ID after reassignment):

Outside function (before call): [10, 20, 30], ID: <original_id>
Inside function (before reassign): [10, 20, 30], ID: <original_id>
Inside function (after reassign): [1, 2, 3, 4], ID: <new_id>
Outside function (after call): [10, 20, 30], ID: <original_id>

Python offers flexibility in how arguments can be passed to functions:

  • Positional Arguments: Passed in the order they are defined in the function signature. The number of arguments must match unless defaults are specified.
  • Keyword Arguments: Passed using parameter_name=value syntax. The order doesn’t matter, and they can be mixed with positional arguments (positional must come first).
  • Default Arguments: Parameters are given a default value in the function definition (def func(param=default):). If the caller doesn’t provide a value for that parameter, the default is used.
  • Variable-Length Positional Arguments (*args): Collects any extra positional arguments into a tuple.
  • Variable-Length Keyword Arguments (**kwargs): Collects any extra keyword arguments into a dictionary.

More advanced (Python 3+): Positional-only (/) and keyword-only (*) arguments enforce how arguments can be passed.

These must be provided in the correct order.

#!/usr/bin/env python3
def describe_pet(animal_type: str, pet_name: str):
print(f"I have a {animal_type} named {pet_name}.")
describe_pet("hamster", "Harry") # Correct call
# describe_pet("Whiskers") # TypeError: missing 1 required positional argument: 'pet_name'

Order doesn’t matter when using keywords.

#!/usr/bin/env python3
def describe_pet(animal_type: str, pet_name: str):
print(f"I have a {animal_type} named {pet_name}.")
describe_pet(pet_name="Willie", animal_type="dog") # Order changed, but works
describe_pet("cat", pet_name="Whiskers") # Mixing positional and keyword

Provide flexibility by making some arguments optional. Default values are evaluated once when the function is defined.

Important: Avoid using mutable objects (like lists or dictionaries) as default values, as they can lead to unexpected behavior because the same object is reused across calls. Use None as the default and create the mutable object inside the function if needed.

#!/usr/bin/env python3
def power(base: float, exponent: float = 2.0):
"""Calculates base raised to the exponent power."""
return base ** exponent
print(f"5 squared: {power(5)}") # Uses default exponent=2
print(f"2 cubed: {power(2, 3)}") # Overrides default
print(f"3 squared: {power(base=3)}") # Keyword arg, uses default exponent

Variable-Length Arguments (*args and **kwargs)

Section titled “Variable-Length Arguments (*args and **kwargs)”

Used when you don’t know in advance how many arguments might be passed.

#!/usr/bin/env python3
def make_pizza(size: str, *toppings: str, **customer_info: str):
"""Summarizes the pizza being ordered."""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
if toppings:
for topping in toppings:
print(f"- {topping}")
else:
print("- Plain cheese")
if customer_info:
print("\nCustomer Info:")
for key, value in customer_info.items():
print(f"- {key}: {value}")
# Calling the function
make_pizza("12", "mushrooms", "green peppers", "extra cheese")
make_pizza("16", delivery="yes", name="John Doe")
make_pizza("9", "pepperoni", name="Jane", phone="555-1234", vip=True)

*toppings collects all extra positional arguments (‘mushrooms’, ‘green peppers’, …) into a tuple named toppings.

**customer_info collects all extra keyword arguments (delivery='yes', name='John Doe', …) into a dictionary named customer_info.

Lambda functions provide a way to create small, anonymous functions inline, typically for short operations where a full def statement is overly verbose.

  • Syntax: lambda arguments: expression
  • Can take any number of arguments but can only have one expression.
  • The expression’s value is implicitly returned.
  • Cannot contain complex statements (like assignments, loops, try/except).
  • Often used with functions like map(), filter(), sorted(), or in GUI callbacks.
#!/usr/bin/env python3
# Lambda function to add two numbers
add = lambda x, y: x + y
print(f"Lambda add(5, 3): {add(5, 3)}")
# Using lambda with sorted()
points = [(1, 2), (3, 1), (5, -4), (2, 3)]
# Sort points based on the second element (y-coordinate)
points_sorted_by_y = sorted(points, key=lambda point: point[1])
print(f"Points sorted by y: {points_sorted_by_y}")

The return statement exits the current function execution and sends a value (or None) back to the point where the function was called.

A function can return any type of Python object, including tuples, lists, dictionaries, or even other functions. To return multiple values, simply list them separated by commas; Python automatically packs them into a tuple.

#!/usr/bin/env python3
def calculate_stats(numbers: list) -> tuple:
"""Calculates the sum and average of a list of numbers."""
if not numbers:
return (0, 0.0) # Return tuple for empty list
total = sum(numbers)
average = total / len(numbers)
return total, average # Returns a tuple (total, average)
nums = [10, 20, 30, 40]
stats_result = calculate_stats(nums)
print(f"Stats for {nums}: Sum={stats_result[0]}, Average={stats_result[1]}")
# Or unpack the returned tuple directly
sum_val, avg_val = calculate_stats(nums)
print(f"Unpacked: Sum={sum_val}, Average={avg_val}")

Scope determines the visibility of a variable within different parts of your program. Python uses the LEGB rule to resolve names:

  • Local: Names assigned within a function (and not declared global or nonlocal).
  • Enclosing function locals: Names in the local scope of any enclosing functions (for nested functions).
  • Global: Names assigned at the top level of a module file, or declared global in a function.
  • Built-in: Names preassigned in Python (like print, len, Exception).

Local variables exist only within the function where they are defined. Global variables are accessible throughout the module.

#!/usr/bin/env python3
global_var = 100 # Global scope
def my_func():
local_var = 50 # Local scope
print(f"Inside function: local_var = {local_var}")
print(f"Inside function: global_var = {global_var}") # Can access global
my_func()
print(f"Outside function: global_var = {global_var}")
# print(f"Outside function: local_var = {local_var}") # NameError: name 'local_var' is not defined

Use the global keyword inside a function if you need to modify a global variable, and nonlocal to modify a variable in an enclosing function’s scope (not the global scope).