Functions_Comprehensions_and

1

Defining Functions

▶

Functions are one of the most fundamental building blocks in Python. They allow you to group reusable logic under a single name, making programs easier to read, test, and maintain. Understanding how to define and call functions correctly — and how to document them well — is essential for writing clean, professional Python code.

  • The def keyword signals the start of a function definition. When Python encounters def, it creates a new function object and binds it to the name you provide, but does not execute the body yet.
  • The function name should follow snake_case naming conventions per Python style guides. This means using all lowercase letters with words separated by underscores, for example calculate_total or get_user_input.
  • The colon at the end of the signature is required and introduces the indented body block. Every statement belonging to the function must be consistently indented beneath the def line, and Python will raise an IndentationError if this rule is violated.
  • Defining a function with the same name twice in the same scope silently overwrites the first definition. This can be a subtle source of bugs, especially in larger files, so it is important to keep function names unique within a given scope.
  • return ends function execution and passes the specified value to the caller. As soon as a return statement is reached, the function stops running and control is handed back to wherever the function was called from.
  • Omitting return or writing return alone causes the function to return None. This is Python's way of representing the absence of a meaningful value, and it is easy to accidentally use such a function's result as if it were a real value.
  • A function can contain multiple return statements to handle different conditions. For example, an early return can be used to exit quickly when input is invalid, keeping the main logic uncluttered.
  • The returned value can be assigned to a variable or used directly in an expression. For instance, you can write total = add(3, 4) or embed the call inside a larger expression like print(add(3, 4) * 2).
  • Docstrings are written as triple-quoted strings immediately after the def line. They are the standard way to embed documentation directly into the function itself, rather than relying solely on comments elsewhere in the file.
  • They describe the function's purpose, parameters, and return value. A well-structured docstring answers the questions: what does this function do, what does it expect, and what does it give back?
  • The built-in help() function displays a function's docstring interactively. Calling help(my_function) in the Python REPL renders the docstring in a readable format, making it invaluable during exploration and debugging.
  • Well-written docstrings improve code maintainability and team collaboration. When you or a colleague returns to a function weeks later, a clear docstring removes the need to reverse-engineer the code's intent.
  • Parentheses after the function name trigger execution; without them, the name refers to the function object itself. Writing greet() calls the function, while writing greet without parentheses simply references the object, which can be stored, passed, or printed.
  • Arguments supplied at the call site are matched to the function's parameters. Python takes the values you provide inside the parentheses and maps them onto the corresponding parameter names defined in the function signature.
  • The call expression evaluates to whatever the function returns. This means a function call can appear anywhere a value is expected — in assignments, conditions, print statements, or other function calls.
  • Functions can call other functions, enabling modular and layered program design. Breaking a complex task into smaller functions that call one another keeps each piece focused and independently testable.
# Basic function definition with a docstring
def greet_user(name):
    """Return a personalised greeting string for the given name."""
    return f"Hello, {name}!"

# Calling the function and using its return value
message = greet_user("Alice")
print(message)   # Hello, Alice!

# help() displays the docstring
help(greet_user)
NotesIntroduces the syntax and mechanics of creating reusable functions in Python. Covers the def keyword, naming conventions, and the role of the return statement. Emphasise to students that forgetting the colon or inconsistent indentation are the most common early mistakes. The silent overwrite of a function name is worth a live demonstration.
2

Parameters and Arguments

▶

Once you know how to define a function, the next step is understanding the many ways Python lets you supply values to it. Python offers positional arguments, keyword arguments, and default parameter values — each with its own rules and best-use scenarios. Getting the terminology straight between parameters and arguments also helps you read error messages accurately.

  • Positional arguments must be supplied in the exact order the parameters are declared. Python assigns each argument to the parameter at the same position in the signature, so swapping the order of your values changes what each parameter receives.
  • Too few or too many positional arguments raise a TypeError at runtime. Python's error message will tell you how many arguments were expected versus how many were given, making these mistakes easy to diagnose.
  • Positional parameters are the most common and simplest form of argument passing. They work well when the meaning of each argument is obvious from context or when the function has only one or two parameters.
  • The number of positional arguments must match the number of non-default parameters. Any parameter that does not have a default value must receive an argument at every call site, or Python will raise an error.
  • Keyword arguments use the syntax parameter_name=value at the call site. By naming the parameter you are targeting, you make the intent of each argument explicit without the reader needing to check the function signature.
  • They can be supplied in any order regardless of the parameter declaration order. This flexibility is especially valuable when a function has many parameters and you only need to specify a few of them by name.
  • Mixing positional and keyword arguments is allowed, but positional must precede keyword. Writing func(10, y=20) is valid, but func(x=10, 20) is a SyntaxError because a positional argument cannot follow a keyword argument.
  • Keyword arguments make function calls self-documenting and reduce reliance on documentation. A call like connect(host="localhost", port=8080) communicates its meaning immediately, even without reading the function definition.
  • Default values are assigned with the syntax parameter=value in the function signature. For example, def send(message, retries=3) means retries will be 3 unless the caller provides a different value.
  • Parameters with defaults must appear after all parameters without defaults. Placing a defaulted parameter before a required one is a SyntaxError, because Python would have no way to determine which arguments were intended for which parameters.
  • Default values are evaluated at definition time, not at each call. This means a mutable default like a list is created once and shared across all calls, which can lead to unexpected behaviour if it is modified inside the function.
  • Using None as a default and initialising mutable defaults inside the body avoids shared-state bugs. The idiom if items is None: items = [] inside the function body ensures a fresh list is created on every call that does not supply one.
  • Callers can override a default by supplying the argument explicitly. Defaults represent sensible fallback values, but they should never prevent a caller from customising behaviour when needed.
  • Parameters live in the function signature and act as placeholders for incoming values. They are local variable names that only exist within the function's scope and receive their values when the function is called.
  • Arguments are the concrete values provided at the call site. They are the actual data — numbers, strings, objects — that the function will work with during that particular execution.
  • A function may have zero or more parameters and is called with a matching set of arguments. A function with no parameters, like def say_hello():, must always be called with empty parentheses and no arguments.
  • Error messages from Python typically refer to arguments when describing call mismatches. For example, "takes 2 positional arguments but 3 were given" — understanding this terminology helps you locate and fix the mistake quickly.
# Positional arguments — order matters
def describe_pet(animal, name):
    """Print a simple description of a pet."""
    print(f"I have a {animal} named {name}.")

describe_pet("dog", "Rex")          # positional
describe_pet(name="Rex", animal="dog")  # keyword — order swapped, still works

# Default parameter value
def power(base, exponent=2):
    """Return base raised to exponent (default: squared)."""
    return base ** exponent

print(power(5))       # uses default → 25
print(power(5, 3))    # overrides default → 125

# Avoiding the mutable default trap
def append_item(item, collection=None):
    """Append item to collection safely."""
    if collection is None:
        collection = []
    collection.append(item)
    return collection
NotesExplores the different ways values can be passed into functions, including positional, keyword, and default parameters. Establishes clear terminology between parameters and arguments. The mutable default argument trap is worth a live demonstration — show what happens with <code>def bad(x, items=[])</code> called multiple times. Reinforce the parameter vs. argument distinction early, as students often use the terms interchangeably when asking for help.