functions

Table of Contents

1. characteristics

  • function = predefined code block that can be used in multiple parts of your program.
    • code block can include branches, loops, and other statements.
  • useful for reducing redundancy and improving clarity and readability.
    • on a much larger scale, functions can be defined in one file and potentially be used in other files as needed, allowing for modularity.
  • two components:
    • function definition = consists of function name and block of statements.
    • function call = interpreter jumps to function definition and executes its statements.
  • def keyword is used to define new functions.
  • return statement = returns a value from a function.
    • function that doesn’t return a value simply returns None (keyword).
    • void function = function with no return statement.
      • probably the most popular example: print() function.
  • a function that returns a value can be passed as an argument into another function call.
    • called hierarchical function calls.
    • ics 6b type stuff: similar to composite functions, where the output of one function gets passed in as input into another function.
  • here are a few function characteristics examples:
print("function definition without arguments:")

# function definition without arguments:
def greeting():
    print("Hello World!")

# function call:
greeting() # prints "Hello World!"

print()
print("function call returning None:")

# can use the return value from a function as an argument for another function.
# print statement always returns None
print(print()) # prints an empty line and None

function definition without arguments:
Hello World!

function call returning None:

None

2. arguments

  • parameter = any input mentioned in function definition.
  • argument = value passed into a function parameter when a function is called.
  • a parameter is tied to the argument object until the function returns.
  • a function can have 0 parameters.
  • if a function has multiple parameters, then argument values are assigned to parameters positionally.
    • (first parameter = first argument, second parameter = second argument, …)
  • passing by assignment (AKA passing by object reference):
    • when an argument is passed, the function’s parameter receives a reference to the same object in memory that the argument points to.
      • similar to assigning the value of one variable to another since both variables now point to the same object.
    • immutable objects: any changes made to the parameter’s value only remain changed within the local scope. The original argument passed in will still refer to the unchanged object once the function exits.
    • mutable objects: if the contents of the object are modified in-place, these changes will be visible outside of the function’s scope because the argument and parameter point to the same object.
  • here are a few examples of functions with arguments and return values:
print("function definition with argument:")

# function definition with argument:
def greeting_name(name):
    print(f"Hello {name}!")

# function call with argument:
greeting_name("Professor Alfaro") # "Professor Alfaro" is
                                  # assigned to name when function is called.
greeting_name("World") # "World" is assigned to name when function is called.

# prints out:
# Hello Professor Alfaro!
# Hello World!

print()
print("function definition with return value:")

# function definition with return value:
def add(a, b):
    return a + b

# return values from function calls can be assigned to a variable for future use.
result = add(1, 2)
print(f"Sum: {result}") # prints Sum: 3

# can use the return value from a function as an argument for another function.
print(add(6, 7)) # prints 13

function definition with argument:
Hello Professor Alfaro!
Hello World!

function definition with return value:
Sum: 3
13

3. docstrings

  • string literal placed in the first line of a function definition.
    • use single-line comments for simpler functions.
    • use multi-line comments for complex functions.
      • include function argument descriptions.
  • help() function prints out a function’s docstring, providing useful information on how to use it.
    • works with many built-in Python functions and data types.
  • here are a few examples of function docstrings:
print("single line docstring:")

# must use triple quotes instead of #.
def square(n):
    """returns the square of input n. """
    return n * n

help(square)

print("-" * 50) # used as output separator
print("multi-line docstring:")

def add(a, b):
    """
    adds two numbers together. takes two inputs and returns their sum.

    parameters:
        a (int or float): the first number.
        b (int or float): the second number.

    returns:
        int or float: sum of a and b.
    """
    return a + b

help(add)

single line docstring:
Help on function square in module __main__:

square(n)
    returns the square of input n.

--------------------------------------------------
multi-line docstring:
Help on function add in module __main__:

add(a, b)
    adds two numbers together. takes two inputs and returns their sum.

    parameters:
        a (int or float): the first number.
        b (int or float): the second number.

    returns:
        int or float: sum of a and b.

4. asserts

  • assert statement = raises an AssertionError if the conditional expression is not met (AKA is False).
    • if the conditional expression is True, then the program continues executing like usual.
  • pre-condition asserts = constraint that must be True before a code block can execute correctly.
    • Python may not detect programming errors that are specific to our functions.
    • By using pre-condition asserts, we can enforce additional conditions that Python doesn’t handle on its own.
  • here are a few examples of using assert statements:
# assert statement is True:
x = 10
assert x > 5, "x should be greater than 5"
print("x is greater than 5")

# assert statement is False:
# y = 3
# assert y > 5, "y should be greater than 5" # would raise AssertionError
# print("this line won't print because an AssertionError occurs.")

x is greater than 5
def division(a, b):
    assert b != 0, "Denominator cannot be 0"
    return a / b

# assert statement is True:
print(division(100, 20)) # prints 5
# assert statement is False:
# print(division(1, 0)) # would raise AssertionError

5.0

5. namespaces and scopes

  • namespace = maps names to objects.
    • can view the names in the current local and global nanespace using locals() and globals() functions.
  • scope = the part of a program where a name is visible.
    • built-in scope = built in Python names.
    • global scope = globally defined names that aren’t in any functions.
    • local scope = if a function is being executed, then the function is the local scope. Otherwise, the local scope is the same as the global scope.
  • scope resolution = searching for a name in namespaces.
    • the local namespace is checked first, then the global namespace, and finally, the built-in namespace.
  • multiple variables can share the same name despite having different values if they’re in different namespaces.
    • assignment statements always prioritize creation/modification of a name’s value in the local namespace.
    • however, this can be overridden using the global keyword, which forces the interpreter to consider a local name as a global name.
  • local variable = variable created inside a function.
  • global variable = variable created outside of any functions.
  • generally want to limit the number of global variables in your program to reduce dependencies and allow for code modularity.
    • generally don’t want your local and global variables to have the same names since it can be confusing to others who read your code.
  • here are a few examples demonstrating namespaces, scopes, locals(), globals(), and global:
# demonstrating namespaces and scopes

global_variable = "global variable"

def function1():
    local_variable1 = "local variable 1"
    print("entering function1")
    print(f"accessing global_variable: {global_variable}")
    print(f"accessing local_variable1: {local_variable1}")

def function2():
    local_variable2 = "local variable 2"
    print()
    print("entering function2")
    print(f"accessing global_variable: {global_variable}")
    # print(local_variable1) # would raise a NameError because local_variable1 is not in scope.

print("in global scope:")
print(f"accessing global_variable: {global_variable}")

function1()
function2()

print("built-in scope always works:")
print(len([1, 2, 3]))

# print(local_variable2) # would raise a NameError because local_variable2 is not in scope.

in global scope:
accessing global_variable: global variable
entering function1
accessing global_variable: global variable
accessing local_variable1: local variable 1

entering function2
accessing global_variable: global variable
built-in scope always works:
3
# demonstrating locals() and globals() functions.

global_variable = "global variable"

def a_function():
    local_variable = "local_variable"
    print("inside function:")
    print("calling locals() function:", locals()) # has local_variable
    print("calling globals() function:", globals()) # has global_variable,
                                                    # a_function, and other built-in names

print("in global scope:")
print("locals():", locals()) # has global_variable, a_function, and other built-in names
print("globals():", globals()) # has global_variable, a_function, and other built-in names

a_function()

in global scope:
locals(): {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
globals(): {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
inside function:
calling locals() function: {'local_variable': 'local_variable'}
calling globals() function: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
global_counter = 0

def increment_global_counter():
    # modifies global global_counter since we used the global keyword.
    global global_counter
    global_counter += 1
    print("Inside function, global_counter:", global_counter)

print("Before function call, global_counter:", global_counter)
increment_global_counter()
print("After function call, global_counter:", global_counter)

def try_to_modify_global_without_keyword():
    # modifies local global_counter instead of global global_counter.
    global_counter = 100
    print("Inside function (local modification), global_counter:", global_counter)

try_to_modify_global_without_keyword()
print("After second function call, global_counter:", global_counter) # should still be 1

Before function call, global_counter: 0
Inside function, global_counter: 1
After function call, global_counter: 1
Inside function (local modification), global_counter: 100
After second function call, global_counter: 1

Author: Ashley Pock

Created: 11/12/2025 Wed