Functions & Modular Programming

Session 4 — Chapter 7: Defining Functions, Return Values, Arbitrary Arguments, Lambda & Modules

SEC1302 · Python for Information Security

>>> def verify_checksum(payload, expected_hash):
... import hashlib
... digest = hashlib.sha256(payload.encode()).hexdigest()
... return digest == expected_hash
...
>>> verify_checksum("admin_token", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
False

Agenda

Defining Functions, Arguments & Return Values

7.1 Defining a Function

  • Anatomy of def, docstrings, and function execution
  • Passing information: parameters vs. arguments

7.2 Passing Arguments

  • Positional arguments and order dependency
  • Keyword arguments and default parameter values
  • Equivalent calls and avoiding argument errors

7.3 Return Values

  • Returning simple values and optional parameters
  • Returning dictionaries and structured objects
  • Using functions with interactive while loops

Data Mutation, Arbitrary Arguments, Lambda & Modules

7.4 Passing Lists and Mutable Data

  • Modifying lists inside functions
  • Preventing mutation via slicing and defensive copying

7.5 Passing an Arbitrary Number of Arguments

  • Positional arbitrary arguments (*args)
  • Keyword arbitrary arguments (**kwargs)

7.6 Lambda Functions & Functional Operations

  • Syntax, map(), filter(), and custom sort keys
  • Pandas Series and DataFrame lambda transformations

7.7 Storing Functions in Modules

  • Import strategies, aliases (as), and namespace management
  • PEP 8 style guide, type hints, and security practices

Session Learning Outcomes

1

Deconstruct and Implement Modular Code

Define reusable functions using def, docstrings, positional arguments, keyword arguments, and default parameters.

2

Manage Data Flow and Scope

Implement single and composite return values (dictionaries, tuples), while controlling variable scope and preventing accidental mutation of mutable arguments.

3

Utilize Advanced Argument Packing and Functional Tools

Apply *args and **kwargs for flexible interfaces, and leverage anonymous lambda functions with map(), filter(), and sorted().

4

Organize Modular Security Software

Package routines into importable modules, manage namespaces cleanly, implement type hints, and apply defensive coding principles to avoid common security vulnerabilities.

Mapped to CLO1 — apply modular Python programming, procedural abstraction, and functional tools to security automation scripts (C3-P2).

What Is a Function?

A function is a named, reusable block of code designed to perform a specific, cohesive task.

Functions provide procedural abstraction: the caller relies on the interface and expected output without needing to track internal implementation details.

Eliminates code duplication (DRY principle: Don't Repeat Yourself), enhances readability, and enables unit testing.

def greet_user():
    """Display a simple greeting."""
    print("Hello!")

greet_user()
Hello!

Execution Lifecycle

1

Definition phase (def)

Python compiles the block into a code object and binds it to the function name in the local namespace.

2

Call phase (greet_user())

The call operator () allocates a new execution frame on the call stack, executes the bytecode, and returns control upon completion.

Sources: https://docs.python.org/3/tutorial/controlflow.html#defining-functions

Passing Information to a Function

Functions become flexible utilities when they accept input data dynamically from the caller.

Adding variable names inside the parentheses of the def statement configures the function to accept external inputs.

The received value is bound to the local variable for the duration of the function call.

def greet_user(username):
    """Display a personalized greeting."""
    print(f"Hello, {username.title()}!")

greet_user('jesse')
greet_user('sarah')
Hello, Jesse!
Hello, Sarah!

Technical observation

Each invocation creates an isolated execution scope. In the first call, username references 'jesse'; in the second call, it references 'sarah'.

Sources: https://docs.python.org/3/reference/compound_stmts.html#function-definitions

Parameters vs. Arguments

While frequently used interchangeably in informal discussion, formal computer science maintains a precise distinction between parameters and arguments.

Parameter: A variable listed inside the parentheses of a function definition. It defines the specification of what data the function expects.

Argument: A concrete value or object reference passed to the function during invocation in a function call.

s

Positional Arguments

The simplest mechanism for passing arguments maps each caller argument to a function parameter based strictly on ordinal order.

Python binds the first argument to the first parameter, the second argument to the second parameter, and continues sequentially.

def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('hamster', 'harry')
I have a hamster.
My hamster's name is Harry.

Sources: https://docs.python.org/3/tutorial/controlflow.html#positional-arguments

Reusability: Multiple Function Invocations

Once defined, a function can be invoked repeatedly across different execution paths, processing diverse datasets with zero code duplication.

Modular decomposition separates business logic into self-contained components that can be composed into larger security audit pipelines.

def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"I have a {animal_type}. Name: {pet_name.title()}.")

describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')
describe_pet('cat', 'lucy')
I have a hamster. Name: Harry.
I have a dog. Name: Willie.
I have a cat. Name: Lucy.

Sources: https://docs.python.org/3/tutorial/controlflow.html#defining-functions

Keyword Arguments

A keyword argument is a name-value pair passed directly in the function call.

The caller explicitly specifies which parameter receives each value by name (parameter_name=value).

Keyword arguments liberate the caller from strict positional ordering, eliminating semantic confusion.

def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
I have a hamster.
My hamster's name is Harry.
I have a hamster.
My hamster's name is Harry.

Sources: https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments

Default Parameter Values

Functions can define default values for one or more parameters in the def statement header.

If the caller provides an explicit argument, Python uses the caller's value; if omitted, Python automatically applies the default fallback.

def describe_pet(pet_name, animal_type='dog'):
    """Display pet information with a default animal type."""
    print(f"I have a {animal_type}. Name: {pet_name.title()}.")

describe_pet(pet_name='willie')
describe_pet('willie')
describe_pet(pet_name='harry', animal_type='hamster')
I have a dog. Name: Willie.
I have a dog. Name: Willie.
I have a hamster. Name: Harry.

Sources: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values

Equivalent Calls & Handling Argument Errors

Default parameters and keyword flexibility allow a single function to be called using multiple equivalent syntax patterns.

def describe_pet(pet_name, animal_type='dog'):
    """Summarize pet attributes."""
    return f"{animal_type}: {pet_name.title()}"

print(describe_pet('willie'))
print(describe_pet(pet_name='willie'))
print(describe_pet('harry', 'hamster'))
print(describe_pet(pet_name='harry', animal_type='hamster'))
dog: Willie
dog: Willie
hamster: Harry
hamster: Harry

Sources: https://docs.python.org/3/tutorial/errors.html

Return Values: The return Statement

A function is not limited to printing information directly to stdout; it can process data and send values back to the calling line via return.

When Python encounters return, it terminates function execution immediately and hands the resulting value back to the caller.

def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')
print(musician)
Jimi Hendrix

Sources: https://docs.python.org/3/reference/simple_stmts.html#the-return-statement

Making Arguments Optional

Optional arguments allow functions to handle varying degrees of input completeness without requiring separate overloaded definitions.

Designers assign an empty string ('') or None as the default value, using conditional checks to adapt processing logic.

def get_formatted_name(first_name, last_name, middle_name=''):
    """Return a formatted name with an optional middle component."""
    if middle_name:
        full_name = f"{first_name} {middle_name} {last_name}"
    else:
        full_name = f"{first_name} {last_name}"
    return full_name.title()

print(get_formatted_name('jimi', 'hendrix'))
print(get_formatted_name('john', 'hooker', 'lee'))
Jimi Hendrix
John Lee Hooker

Sources: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values

Returning Complex Structures: Dictionaries

Functions can construct and return complex data structures, including lists, tuples, and dictionaries.

Returning a dictionary bundles disparate attributes into a single structured, key-indexed record.

def build_person(first_name, last_name, age=None):
    """Return a dictionary of information about a person."""
    person = {'first': first_name, 'last': last_name}
    if age is not None:
        person['age'] = age
    return person

musician_1 = build_person('jimi', 'hendrix')
musician_2 = build_person('jimi', 'hendrix', age=27)
print(musician_1)
print(musician_2)
{'first': 'jimi', 'last': 'hendrix'}
{'first': 'jimi', 'last': 'hendrix', 'age': 27}

Sources: https://docs.python.org/3/tutorial/datastructures.html#dictionaries

Returning Multiple Values via Tuples

In Python, a function can return multiple values separated by commas in a single return statement.

Python automatically packs multiple comma-separated values into a single tuple; the caller can unpack them directly.

def parse_network_socket(socket_address):
    """Split an IP:port address string into separate components."""
    ip_addr, port_str = socket_address.split(':')
    return ip_addr, int(port_str)

ip, port = parse_network_socket("192.168.1.100:8080")
result_tuple = parse_network_socket("10.0.0.1:22")
print(f"IP: {ip}, Port: {port}")
print(f"Packed return: {result_tuple} (Type: {type(result_tuple).__name__})")
IP: 192.168.1.100, Port: 8080
Packed return: ('10.0.0.1', 22) (Type: tuple)

Sources: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences

Using Functions with while Loops

Functions can be combined with iterative control structures to power interactive command-line interfaces.

The while loop manages the interactive session and sentinel checks, delegating core data processing to dedicated functions.

def format_user_record(username, role):
    """Normalize and validate user privilege record."""
    return f"User '{username.lower()}' assigned role: {role.upper()}"

user_inputs = [("alice", "admin"), ("bob", "analyst"), ("quit", "")]
audit_log = []
for u_name, u_role in user_inputs:
    if u_name == 'quit':
        break
    record = format_user_record(u_name, u_role)
    audit_log.append(record)

for entry in audit_log:
    print(entry)
User 'alice' assigned role: ADMIN
User 'bob' assigned role: ANALYST

Sources: https://docs.python.org/3/tutorial/controlflow.html

Pure Functions vs. Side Effects

A pure function satisfies two mathematical criteria:

  1. Determinism: Given identical inputs, it always produces identical output.
  1. Zero side effects: It does not modify external variables, mutate mutable arguments, or execute uncontained I/O operations.

An impure function relies on or alters state outside its local scope (e.g., global variables, file writes, terminal output).

Sources: https://docs.python.org/3/howto/functional.html

Pure Function vs. Impure Function

A Pure Function satisfies both of the following conditions:

  1. Determinism: the same input always produces the same output.
  1. Zero Side Effects: it does not change external data, does not directly modify its passed-in parameters, and does not perform I/O such as print() or writing to a file.

An Impure Function violates at least one of the two conditions above.

Pure

def add(a, b):
    return a + b

# add(2, 3) always returns 5, without affecting any external data

Impure

total = 0

def add(a):
    global total
    total += a
    return total

# Calling add(2) repeatedly returns 2, 4, 6... because the function modifies the external variable total

Pure Functions make programs easier to predict, test, and maintain; Impure Functions are still necessary when interacting with the outside world, such as reading/writing files or displaying output with print().

Security Practice: Input Sanitization Functions

Information security tools frequently ingest untrusted user inputs (usernames, domain queries, URL parameters).

Functions provide defensive boundaries by sanitizing, validating, and normalizing raw strings before execution.

import re

def sanitize_identifier(raw_input):
    """Enforce strict alphanumeric whitelist on system identifiers."""
    cleaned = re.sub(r'[^a-zA-Z0-9_-]', '', raw_input.strip())
    if not cleaned:
        raise ValueError("Invalid identifier: contains no valid alphanumeric characters.")
    return cleaned.lower()

test_inputs = ["admin_user", "guest; DROP TABLE users;--", "SOC-analyst_01"]
for item in test_inputs:
    sanitized = sanitize_identifier(item)
    print(f"Raw: '{item}' -> Sanitized: '{sanitized}'")
Raw: 'admin_user' -> Sanitized: 'admin_user'
Raw: 'guest; DROP TABLE users;--' -> Sanitized: 'guestdroptableusers--'
Raw: 'SOC-analyst_01' -> Sanitized: 'soc-analyst_01'

Sources: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html

Security Practice: Cryptographic Digest Utilities

Cryptographic hash functions map variable-length data streams into fixed-size digest strings.

Encapsulating hashing workflows within parameterized functions guarantees standard encoding and uniform algorithm selection.

import hashlib

def compute_sha256(data_string):
    """Compute hex-encoded SHA-256 digest of an input string."""
    encoded_bytes = data_string.encode('utf-8')
    hasher = hashlib.sha256(encoded_bytes)
    return hasher.hexdigest()

test_payload = "password123"
digest = compute_sha256(test_payload)
print(f"Payload: {test_payload}")
print(f"SHA-256: {digest}")
Payload: password123
SHA-256: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f

Sources: https://docs.python.org/3/library/hashlib.html | https://csrc.nist.gov/publications/detail/fips/180-4/final

Passing Sequences: Lists in Functions

When passing a list to a function, the function receives a direct reference to the list object.

The function can iterate over elements, calculate metrics, or format entries efficiently without duplicating data in memory.

def add_user(names):
    names.append("alice")

usernames = ["hannah", "ty"]
add_user(usernames)

print(usernames)
# ['hannah', 'ty', 'alice']
['hannah', 'ty', 'alice']

Sources: https://docs.python.org/3/tutorial/datastructures.html

Modifying a List Inside a Function

Because lists are mutable objects, any modifications made to the list parameter inside the function body alter the original list in the caller's scope.

Common computational pattern: transferring elements between two lists (queue processing).

def print_models(unprinted_designs, completed_models):
    """Simulate printing designs until none are left, moving to completed."""
    while unprinted_designs:
        current_design = unprinted_designs.pop()
        completed_models.append(current_design)

unprinted = ['phone case', 'robot pendant', 'dodecahedron']
completed = []
print_models(unprinted, completed)
print(f"Remaining in queue: {unprinted}")
print(f"Completed models: {completed}")
Remaining in queue: []
Completed models: ['dodecahedron', 'robot pendant', 'phone case']

Sources: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists

Preventing List Modification: Defensive Copying

When a function needs to consume or alter list elements but the caller requires the original dataset intact, pass a slice copy.

  • The slice syntax list_name[:] produces a shallow copy of the entire list.
def process_queue(work_list):
    """Consume elements from a queue."""
    while work_list:
        item = work_list.pop()
        print(f"Processed: {item}")

original_queue = ['task_alpha', 'task_beta', 'task_gamma']
process_queue(original_queue[:])
print(f"Original queue preserved: {original_queue}")
Processed: task_gamma
Processed: task_beta
Processed: task_alpha
Original queue preserved: ['task_alpha', 'task_beta', 'task_gamma']

Sources: https://docs.python.org/3/library/copy.html

Example: Without Slice Copy [:] — The Original List Gets Emptied

Passing the list directly lets the function consume the caller's data.

def process_queue(work_list):
    while work_list:
        item = work_list.pop()
        print(f"Processed: {item}")

original_queue = ['task_alpha', 'task_beta', 'task_gamma']
process_queue(original_queue)  # No [:] used
print(f"Original queue: {original_queue}")
Processed: task_gamma
Processed: task_beta
Processed: task_alpha
Original queue: []

The Mutable Default Argument Pitfall

Crucial Python gotcha: Default parameter expressions are evaluated once when the function is defined, NOT each time the function is called.

If a mutable object (like a list or dict) is used as a default, all calls without an explicit argument share the exact same object.

# Anti-pattern (shared accumulator):
def append_event_bug(event, log=[]):
    log.append(event)
    return log

# Defensive idiomatic fix:
def append_event_fixed(event, log=None):
    if log is None:
        log = []
    log.append(event)
    return log

print("Buggy calls:", append_event_bug("login"), append_event_bug("logout"))
print("Fixed calls:", append_event_fixed("login"), append_event_fixed("logout"))
Buggy calls: ['login', 'logout'] ['login', 'logout']
Fixed calls: ['login'] ['logout']

Sources: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values

Arbitrary Positional Arguments: *args

When a function designer cannot anticipate how many positional arguments a caller might provide, prefix a parameter with an asterisk (*args).

Python packs all excess positional arguments into an immutable tuple bound to that parameter name.

def make_pizza(*toppings):
    """Print the list of toppings that have been requested."""
    print(f"Packed tuple: {toppings} (Count: {len(toppings)})")
    for topping in toppings:
        print(f"- {topping}")

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
Packed tuple: ('pepperoni',) (Count: 1)
- pepperoni
Packed tuple: ('mushrooms', 'green peppers', 'extra cheese') (Count: 3)
- mushrooms
- green peppers
- extra cheese

Sources: https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists

Memory Model: Mutable vs. Immutable Arguments

Understanding Python's argument passing mechanism requires differentiating how mutable and immutable types behave upon reassignment and modification.

Sources

https://docs.python.org/3/reference/datamodel.html#objects-values-and-types

Pass by Value: Python vs. C++

Both languages print 5, but the memory mechanism behind it is different.

Python (int – immutable)

def change(x):
    x = x + 10

a = 5
change(a)
print(a)  # 5

C++ – Pass by Value

#include <iostream>
using namespace std;

void change(int x) {
    x = x + 10;
    cout << x << endl;
}

int main() {
    int a = 5;
    change(a);
    cout << a; // 5
    return 0;
}
  • Python: x initially references the object 5, then x is rebound to a new object 15. The variable a is unaffected.
  • C++: x receives a copy of a's value, so changing x does not affect a.

Memory Map: Pass by Value

How the int 5 is stored while change(a) runs (addresses are illustrative).

Python (int – immutable): names → objects

C++ (pass by value): variables hold values

Pass by Reference: C++ vs. Python

To let a function modify the caller's variable directly, C++ uses the & symbol.

C++ – Pass by Reference

#include <iostream>
#include <string>
using namespace std;

void change(int &x) {
    x = x + 10;
    cout << x << endl;      // 15
}

void change(string &s) {
    s += " World";
    cout << s << endl;      // Hello World
}

int main() {
    int a = 5;
    change(a);
    cout << a << endl;      // 15

    string name = "Hello";
    change(name);
    cout << name << endl;   // Hello World
    return 0;
}

Python – Closest Equivalent (str: immutable, list: mutable)

def change(s):
    s += " World"   # str is immutable → new object, s is rebound

name = "Hello"
change(name)
print(name)  # Hello

def add_ten(nums):
    nums[0] = nums[0] + 10   # list is mutable → changed in place

a = [5]
add_ten(a)
print(a[0])  # 15

Memory Map: Pass by Reference

A reference is an alias for the caller's variable in C++; Python's closest behavior depends on mutability (addresses are illustrative).

C++ (int &x): x is an alias of a

Python: immutable vs. mutable

Pass by Pointer: C++ vs. Python

A pointer parameter receives the address of the caller's variable, so the function can modify the original through dereferencing.

C++ – Pass by Pointer

#include <iostream>
#include <string>
using namespace std;

void change(int *x) {
    *x = *x + 10;
    cout << *x << endl;     // 15
}

void change(string *s) {
    *s += " World";
    cout << *s << endl;     // Hello World
}

int main() {
    int a = 5;
    change(&a);
    cout << a << endl;      // 15

    string name = "Hello";
    change(&name);
    cout << name << endl;   // Hello World
    return 0;
}

Python – No Pointers (int: immutable)

def change(x):
    x = x + 10   # int is immutable → new object, x is rebound

a = 5
change(a)
print(a)  # 5
print(id(a))  # object identity, not a pointer
  • &a passes the address of a; *x dereferences the pointer to reach the original value.
  • The pointer itself is passed by value (a copy of the address), but both copies point to the same variable.
  • Python has no pointer arithmetic or dereferencing: names are bound to objects, and the model is object reference.
  • Python note: int and str are immutable, so the caller's value can't be changed by reassigning the parameter; mutable objects (list, dict) can be changed in place.

Memory Map: Pass by Pointer

A C++ pointer stores the address of the caller's variable; Python has no equivalent (addresses are illustrative).

C++ (int *x): x holds the address of a

Python (int – immutable): no pointers

Conclusion: Python vs. C++ Parameter Passing

Security Practice: Packet Header Inspection Queue

Network intrusion detection routines process ingested packet streams through sequential inspection filters.

Functions manage the sanitization and filtering of packet queues without exposing raw socket memory.

def inspect_packet_queue(raw_packets, suspicious_alerts):
    """Analyze raw packet port headers and quarantine suspicious traffic."""
    while raw_packets:
        pkt = raw_packets.pop(0)
        port = pkt.get('port', 0)
        # Flag well-known insecure legacy cleartext protocols: Telnet(23), HTTP(80)
        if port in (23, 80):
            suspicious_alerts.append(f"FLAGGED: Insecure port {port} from {pkt.get('src')}")

traffic_stream = [
    {'src': '10.0.0.5', 'port': 443},
    {'src': '192.168.1.50', 'port': 23},
    {'src': '172.16.0.12', 'port': 80}
]
alerts = []
inspect_packet_queue(traffic_stream, alerts)
for alert in alerts:
    print(alert)
FLAGGED: Insecure port 23 from 192.168.1.50
FLAGGED: Insecure port 80 from 172.16.0.12

Sources: https://docs.python.org/3/library/collections.html#collections.deque

Mixing Positional and Arbitrary Arguments

Functions can combine standard required positional parameters with an arbitrary positional parameter.

The parameter accepting arbitrary arguments must be placed last in the parameter list.

Python matches positional arguments first; any remaining values are collected into the *args tuple.

def make_pizza(size, *toppings):
    """Summarize the pizza we are about to make."""
    print(f"Making a {size}-inch pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

Sources: https://peps.python.org/pep-3102/

Arbitrary Keyword Arguments: **kwargs (keyword args)

When a function needs to accept an arbitrary number of named keyword arguments, prefix the parameter with double asterisks (**kwargs).

Python packs all excess keyword arguments into a standard dictionary, mapping argument names (strings) to their passed values.

def build_profile(first, last, **user_info):
    """Build a dictionary containing everything we know about a user."""
    user_info['first_name'] = first
    user_info['last_name'] = last
    return user_info

user_profile = build_profile(
    'albert', 'einstein',
    location='princeton',
    field='physics'
)
print(user_profile)
{'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}

Sources: https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments

Complete Parameter Hierarchy & Ordering Rules

When combining multiple parameter kinds in a single function header, Python enforces a strict declaration order.

Parameter declaration order:

  1. Standard positional-or-keyword parameters (without defaults)
  1. Positional parameters with default values
  1. Arbitrary positional arguments (*args)
  1. Keyword-only parameters (with or without defaults)
  1. Arbitrary keyword arguments (**kwargs)
def configure_firewall(rule_id, interface='eth0', *ports, log=True, **metadata):
    """Demonstrate full parameter spectrum."""
    return {
        'id': rule_id,
        'iface': interface,
        'ports': ports,
        'log': log,
        'meta': metadata
    }

config = configure_firewall(101, 'eth1', 80, 443, 8080, log=False, author='sec_ops', zone='dmz')
print(config)
{'id': 101, 'iface': 'eth1', 'ports': (80, 443, 8080), 'log': False, 'meta': {'author': 'sec_ops', 'zone': 'dmz'}}

Sources: https://docs.python.org/3/reference/compound_stmts.html#function-definitions

Argument Unpacking: Expanding Collections

The asterisk operators perform the reverse operation at the call site: unpacking existing collections into individual arguments.

Single asterisk * unpacks a list, tuple, or set into individual positional arguments.

Double asterisk ** unpacks a dictionary into individual keyword arguments.

def scan_target(host, port, timeout):
    """Simulate scanning a specific host and port."""
    return f"Scanning {host}:{port} (timeout={timeout}s)"

target_list = ["192.168.1.1", 443, 5]
target_dict = {"host": "10.0.0.1", "port": 22, "timeout": 2}

print("Unpacked list: ", scan_target(*target_list))
print("Unpacked dict: ", scan_target(**target_dict))
Unpacked list:  Scanning 192.168.1.1:443 (timeout=5s)
Unpacked dict:  Scanning 10.0.0.1:22 (timeout=2s)

Sources: https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists

Security Practice: Flexible Network Audit Interface

Production cybersecurity utilities (like Nmap automation scripts) require robust argument handling to accept variable target scopes and flags.

Demonstrates real-world application of *args for multiple target ports and **kwargs for probe configuration options.

def audit_host(ip_address, *target_ports, **options):
    """Simulate multi-port security probe with dynamic options."""
    verbose = options.get('verbose', False)
    protocol = options.get('protocol', 'TCP')
    results = []
    for port in target_ports:
        status = "OPEN" if port in (80, 443, 22) else "FILTERED"
        if verbose:
            results.append(f"[{protocol}] {ip_address}:{port} -> {status}")
        else:
            results.append(f"{port}:{status}")
    return results

scan_summary = audit_host('192.168.1.254', 21, 22, 80, protocol='TCP', verbose=True)
for item in scan_summary:
    print(item)
[TCP] 192.168.1.254:21 -> FILTERED
[TCP] 192.168.1.254:22 -> OPEN
[TCP] 192.168.1.254:80 -> OPEN

Sources: https://docs.python.org/3/library/socket.html

Introduction to Lambda Functions

In Python, lambda expressions create small, anonymous (unnamed) inline functions on the fly.

Syntax: lambda arguments: single_expression

Key technical constraints:

  • Can accept any number of arguments (comma-separated).
  • Can only contain a single expression whose evaluated result is automatically returned.
  • Cannot contain statements, loops, assignments, or multiline logic.
# Standard function:
def double_standard(x):
    return x * 2

# Equivalent lambda expression:
double_lambda = lambda x: x * 2

print("Standard function call:", double_standard(5))
print("Lambda function call:  ", double_lambda(5))
Standard function call: 10
Lambda function call:  10

Sources: https://docs.python.org/3/reference/expressions.html#lambda

Lambda Functions vs. Standard def Functions

Comparing lambda expressions against traditional def functions reveals distinct engineering trade-offs regarding scope and maintainability.

Sources: https://peps.python.org/pep-0008/#programming-recommendations

Filtering Sequences: filter() with Lambda

The built-in filter(function, iterable) constructs an iterator from elements of an iterable for which the function returns True.

An inline lambda provides a concise predicate function without requiring a named helper function.

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Filter even numbers:
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))

# Security application: filter high privileged ports (< 1024):
ports = [21, 22, 80, 443, 8080, 8443, 9000]
privileged_ports = list(filter(lambda p: p < 1024, ports))

print(f"Even numbers: {even_numbers}")
print(f"Privileged ports: {privileged_ports}")
Even numbers: [2, 4, 6, 8, 10]
Privileged ports: [21, 22, 80, 443]

Sources: https://docs.python.org/3/library/functions.html#filter

Transforming Sequences: map() with Lambda

The built-in map(function, iterable) applies a specified transformation function to every item in an iterable.

Combines seamlessly with lambda expressions for element-wise mathematical or formatting operations.

numbers = [1, 2, 3, 4, 5]

# Calculate cubes:
cubes = list(map(lambda x: pow(x, 3), numbers))

# Security application: sanitize IP list into subnet broadcast targets:
host_suffixes = [10, 20, 30, 40]
ip_targets = list(map(lambda host: f"192.168.1.{host}", host_suffixes))

print(f"Cubes: {cubes}")
print(f"IP targets: {ip_targets}")
Cubes: [1, 8, 27, 64, 125]
IP targets: ['192.168.1.10', '192.168.1.20', '192.168.1.30', '192.168.1.40']

Sources: https://docs.python.org/3/library/functions.html#map

Custom Sorting with Lambda key Functions

Both list.sort() (in-place) and sorted() (returns new list) accept an optional key parameter.

The key parameter specifies a one-argument function that extracts a comparison key from each element before sorting.

# List of tuples: (Service, Port, Risk Score)
services = [("HTTP", 80, 8.5), ("SSH", 22, 4.2), ("FTP", 21, 9.8), ("HTTPS", 443, 2.1)]

# Sort by Port number (index 1):
by_port = sorted(services, key=lambda s: s[1])

# Sort by Risk Score descending (index 2):
by_risk = sorted(services, key=lambda s: s[2], reverse=True)

print("Sorted by Port: ", by_port)
print("Sorted by Risk: ", by_risk)
Sorted by Port:  [('FTP', 21, 9.8), ('SSH', 22, 4.2), ('HTTP', 80, 8.5), ('HTTPS', 443, 2.1)]
Sorted by Risk:  [('FTP', 21, 9.8), ('HTTP', 80, 8.5), ('SSH', 22, 4.2), ('HTTPS', 443, 2.1)]

Sources: https://docs.python.org/3/howto/sorting.html#key-functions

Lambda in Data Analysis: Pandas Integration

In security data science and tabular log analysis, Pandas Series and DataFrame objects utilize .apply() with lambda expressions.

Enables vector-like row-wise or column-wise data transformations across millions of records.

# Conceptual pure-Python demonstration of DataFrame column transformation:
dataset = [
    {'name': 'Luke', 'birthyear': 1976},
    {'name': 'Gina', 'birthyear': 1984},
    {'name': 'Sam',  'birthyear': 2008},
    {'name': 'Emma', 'birthyear': 2012}
]

CURRENT_YEAR = 2026

# Transform records using lambda projection:
for row in dataset:
    row['age'] = (lambda yr: CURRENT_YEAR - yr)(row['birthyear'])

# Filter adult accounts:
adults = list(filter(lambda r: r['age'] >= 18, dataset))
for adult in adults:
    print(f"Name: {adult['name']}, Age: {adult['age']}")
Name: Luke, Age: 50
Name: Gina, Age: 42

Pandas equivalent syntax:

  • df['age'] = df['birthyear'].apply(lambda x: CURRENT_YEAR - x)
  • df[df['age'] >= 18]

Sources: https://pandas.pydata.org/docs/reference/api/pandas.Series.apply.html

Comprehensions vs. map() & filter() with Lambda

Modern idiomatic Python frequently prefers list, dict, and set comprehensions over map() and filter() paired with lambda expressions.

Comprehensions are often faster and significantly more legible to human code reviewers.

Engineering guideline:

  • Use lambda for key functions in sorted() and min()/max(), or lightweight callbacks.
  • Use comprehensions for general sequence transformation and filtering.

Sources: https://peps.python.org/pep-0202/ | https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions

Security Practice: Threat Intelligence Log Analysis

Security Operations Centers (SOC) process high-velocity telemetry streams, isolating high-severity security incidents.

Demonstrates sorting and filtering security event records using lambda expressions.

events = [
    {"src": "10.0.0.12",    "event": "Failed SSH",           "severity": 2},
    {"src": "192.168.1.99", "event": "SQL Injection attempt", "severity": 5},
    {"src": "10.0.0.15",    "event": "Port sweep",            "severity": 3},
    {"src": "172.16.0.4",   "event": "Privilege escalation",  "severity": 5}
]

# 1. Filter critical events (severity >= 4):
critical_alerts = list(filter(lambda e: e["severity"] >= 4, events))

# 2. Sort critical alerts by IP source address:
critical_alerts.sort(key=lambda e: e["src"])

for alert in critical_alerts:
    print(f"[SEV {alert['severity']}] {alert['src']}: {alert['event']}")
[SEV 5] 172.16.0.4: Privilege escalation
[SEV 5] 192.168.1.99: SQL Injection attempt

Sources: https://csrc.nist.gov/publications/detail/sp/800-92/final

Modular Architecture: Storing Functions in Files

As software projects grow, keeping all code in a single file becomes unmanageable and error-prone.

A module in Python is simply a file with a .py extension containing Python definitions and statements.

Modular architecture provides:

  • Separation of Concerns: Isolates algorithmic utilities from main runtime scripts.
  • Reusability: Multiple scripts can import the same utility library.
  • Namespace Partitioning: Prevents global variable collisions.
project_root/
├── network_scanner.py      # Main application script
├── analyzers/
│   └── packet_utils.py     # Module containing helper functions
└── config.py               # Configuration module

Sources: https://docs.python.org/3/tutorial/modules.html

Importing an Entire Module: import module_name

The statement import module_name loads the specified module and binds its module object to the local namespace.

To access functions inside the module, use dot notation: module_name.function_name().

# File: pizza.py
def make_pizza(size, *toppings):
    print(f"Making a {size}-inch pizza with toppings: {', '.join(toppings)}")

# File: making_pizzas.py
import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Making a 16-inch pizza with toppings: pepperoni
Making a 12-inch pizza with toppings: mushrooms, green peppers, extra cheese

Sources: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

Importing Specific Functions: from module import func

To import specific functions directly into the current namespace, use: from module_name import function_name.

When imported this way, the function is invoked directly by its name without dot notation prefix.

Multiple functions can be imported separated by commas: from module_name import func1, func2, func3.

# File: making_pizzas.py
from pizza import make_pizza

make_pizza(16, 'pepperoni')
Making a 16-inch pizza with toppings: pepperoni

Sources: https://docs.python.org/3/tutorial/modules.html#more-on-modules

Aliasing Functions and Modules: The as Keyword

If the name of an imported function or module conflicts with an existing local name, or is excessively long, Python provides the as keyword to assign a concise alias.

Pattern 1: Function Alias

from pizza import make_pizza as mp

mp(16, 'pepperoni')

Pattern 2: Module Alias

import pizza as p

p.make_pizza(12, 'mushrooms', 'extra cheese')

Industry standards — ubiquitous convention in data science and security tooling:

  • import numpy as np
  • import pandas as pd
  • import matplotlib.pyplot as plt
  • import scapy.all as scapy

Sources: https://peps.python.org/pep-0008/#imports

Namespace Pollution: Risks of from module import *

The syntax from module_name import * copies every non-private function and variable from the module into the current local namespace.

While it allows calling functions without module prefixes, it is strongly discouraged in production software.

Risks of wildcard imports:

  1. Namespace Pollution: Overwrites existing local functions or variables silently if names collide.
  1. Loss of Traceability: Static analysis tools and code reviewers cannot determine which module introduced a given function.
  1. Obscure Bugs: Upgrading a third-party library that adds a new function can unexpectedly break existing application code.
from math import *
# Now 'sin', 'cos', 'floor', etc. are injected into global scope,
# shadowing any custom local variables or models named 'sin' or 'floor'.

Sources: https://peps.python.org/pep-0008/#imports | https://docs.python.org/3/tutorial/modules.html#importing-from-a-package

Execution Guard: The __name__ == '__main__' Idiom

Every Python module defines a special built-in string variable named __name__.

When a file is executed directly from the terminal (python3 file.py), Python sets __name__ = '__main__'.

When a file is imported by another module (import file), Python sets __name__ = 'file' (the module's actual filename).

# File: crypto_utils.py
def generate_nonce():
    import secrets
    return secrets.token_hex(16)

# Execution guard block:
if __name__ == '__main__':
    # Unit tests / CLI demo executed ONLY when run directly:
    sample_nonce = generate_nonce()
    print(f"[Self-Test] Generated 16-byte nonce: {sample_nonce}")
[Self-Test] Generated 16-byte nonce: 8f4a13d7e6c9b201a4f89d3e7b1c504a

Sources: https://docs.python.org/3/library/__main__.html

PEP 8 Function Styling & Type Annotations

Professional Python development adheres to formal styling and type annotation standards (PEP 8, PEP 484).

Styling rules:

  1. Naming: Function names must be lowercase with words separated by underscores (snake_case) to improve readability.
  1. Docstrings: Every function definition should immediately be followed by a docstring ("""Docstring""") describing its purpose, parameters, and return value.
  1. Default parameters: Do not place spaces around the = sign when specifying a default parameter value (def func(port=80):).
  1. Modern Type Annotations (PEP 484 / Python 3.9+):
def validate_port(port: int, allow_privileged: bool = False) -> bool:
    """Validate whether a port integer falls within permissible policy limits."""
    if not (1 <= port <= 65535):
        return False
    if not allow_privileged and port < 1024:
        return False
    return True

print("Port 443 (default): ",          validate_port(443))
print("Port 443 (privileged allowed): ", validate_port(443, allow_privileged=True))
print("Port 8080: ",                    validate_port(8080))
Port 443 (default):           False
Port 443 (privileged allowed): True
Port 8080:                    True

Sources: https://peps.python.org/pep-0008/#function-and-variable-names | https://peps.python.org/pep-0484/

Practical Exercises & Real-World Lab Tasks

Hands-on lab problems designed to reinforce Chapter 7 concepts on OnlineGDB:

1

Message Formatter & City-Country Display (Easy)

Write describe_city(city, country='vietnam') with default parameters. Call it with 3 distinct cities to demonstrate keyword and default argument resolution.

2

Music Album & User Profile Builder (Medium)

Implement make_album(artist_name, album_title, track_count=None) returning a structured dictionary. Connect to an interactive while loop allowing continuous entry until 'quit'.

3

Arbitrary Argument Network Audit Tool (Medium - Hard)

Build create_firewall_rule(rule_name, action, *ports, **rule_metadata). Test with variable port lists and custom metadata (e.g., logging=True, direction='inbound').

4

Functional Log Filter (Hard - InfoSec)

Implement a log parser utilizing filter() and sorted() with custom lambda keys to extract and prioritize critical system error codes from raw server logs.

Sources: Syllabus/C7 Functions.docx (Chapter 7 Exercises)

Session Synthesis & Course Roadmap

Summary of foundational paradigms mastered in Session 4:

1

Procedural Abstraction

Decomposing monolithic scripts into focused, single-responsibility functions with clean input/output interfaces.

2

Flexible Calling Signatures

Utilizing positional arguments, keyword arguments, default values, and dynamic *args/**kwargs collection.

3

Memory Awareness

Managing mutable vs. immutable objects and avoiding dangerous shared mutable default arguments.

4

Functional Tools

Streamlining data processing pipelines with lambda expressions, map(), filter(), and sorted() key projections.

5

Modular Code Architecture

Structuring multi-file libraries using import, aliasing (as), and __name__ == '__main__' guards.

Roadmap Preview (Next Session — Chapter 8: Object-Oriented Programming)

  • Transitioning from procedural functions to Object-Oriented Programming (OOP).
  • Modeling real-world security entities (Firewall, Host, Connection) using Classes, Attributes, Methods, and Inheritance.