
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")
False7.1 Defining a Function
7.2 Passing Arguments
7.3 Return Values
7.4 Passing Lists and Mutable Data
7.5 Passing an Arbitrary Number of Arguments
7.6 Lambda Functions & Functional Operations
7.7 Storing Functions in Modules
Define reusable functions using def, docstrings, positional arguments, keyword arguments, and default parameters.
Implement single and composite return values (dictionaries, tuples), while controlling variable scope and preventing accidental mutation of mutable arguments.
Apply *args and **kwargs for flexible interfaces, and leverage anonymous lambda functions with map(), filter(), and sorted().
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).

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!Python compiles the block into a code object and binds it to the function name in the local namespace.
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
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!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
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
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
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
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
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
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: HarrySources: https://docs.python.org/3/tutorial/errors.html
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 HendrixSources: https://docs.python.org/3/reference/simple_stmts.html#the-return-statement
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 HookerSources: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values
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
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
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: ANALYSTSources: https://docs.python.org/3/tutorial/controlflow.html
A pure function satisfies two mathematical criteria:
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
A Pure Function satisfies both of the following conditions:
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 dataImpure
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 totalPure 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().
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
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: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94fSources: https://docs.python.org/3/library/hashlib.html | https://csrc.nist.gov/publications/detail/fips/180-4/final
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
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
When a function needs to consume or alter list elements but the caller requires the original dataset intact, pass a slice copy.
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
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: []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
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 cheeseSources: https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists
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
Both languages print 5, but the memory mechanism behind it is different.
def change(x):
x = x + 10
a = 5
change(a)
print(a) # 5#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;
}How the int 5 is stored while change(a) runs (addresses are illustrative).
To let a function modify the caller's variable directly, C++ uses the & symbol.
#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;
}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]) # 15A reference is an alias for the caller's variable in C++; Python's closest behavior depends on mutability (addresses are illustrative).
A pointer parameter receives the address of the caller's variable, so the function can modify the original through dereferencing.
#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;
}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 pointerA C++ pointer stores the address of the caller's variable; Python has no equivalent (addresses are illustrative).
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.12Sources: https://docs.python.org/3/library/collections.html#collections.deque
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 cheeseSources: https://peps.python.org/pep-3102/
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
When combining multiple parameter kinds in a single function header, Python enforces a strict declaration order.
Parameter declaration order:
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
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
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 -> OPENSources: https://docs.python.org/3/library/socket.html
In Python, lambda expressions create small, anonymous (unnamed) inline functions on the fly.
Syntax: lambda arguments: single_expression
Key technical constraints:
# 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: 10Sources: https://docs.python.org/3/reference/expressions.html#lambda
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
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
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
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
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: 42Pandas equivalent syntax:
Sources: https://pandas.pydata.org/docs/reference/api/pandas.Series.apply.html
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:
Sources: https://peps.python.org/pep-0202/ | https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
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 attemptSources: https://csrc.nist.gov/publications/detail/sp/800-92/final
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:
project_root/
├── network_scanner.py # Main application script
├── analyzers/
│ └── packet_utils.py # Module containing helper functions
└── config.py # Configuration moduleSources: https://docs.python.org/3/tutorial/modules.html
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 cheeseSources: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
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: pepperoniSources: https://docs.python.org/3/tutorial/modules.html#more-on-modules
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.
from pizza import make_pizza as mp
mp(16, 'pepperoni')import pizza as p
p.make_pizza(12, 'mushrooms', 'extra cheese')Industry standards — ubiquitous convention in data science and security tooling:
Sources: https://peps.python.org/pep-0008/#imports
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:
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
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: 8f4a13d7e6c9b201a4f89d3e7b1c504aSources: https://docs.python.org/3/library/__main__.html
Professional Python development adheres to formal styling and type annotation standards (PEP 8, PEP 484).
Styling rules:
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: TrueSources: https://peps.python.org/pep-0008/#function-and-variable-names | https://peps.python.org/pep-0484/
Hands-on lab problems designed to reinforce Chapter 7 concepts on OnlineGDB:
Write describe_city(city, country='vietnam') with default parameters. Call it with 3 distinct cities to demonstrate keyword and default argument resolution.
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'.
Build create_firewall_rule(rule_name, action, *ports, **rule_metadata). Test with variable port lists and custom metadata (e.g., logging=True, direction='inbound').
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)
Summary of foundational paradigms mastered in Session 4:
Decomposing monolithic scripts into focused, single-responsibility functions with clean input/output interfaces.
Utilizing positional arguments, keyword arguments, default values, and dynamic *args/**kwargs collection.
Managing mutable vs. immutable objects and avoiding dangerous shared mutable default arguments.
Streamlining data processing pipelines with lambda expressions, map(), filter(), and sorted() key projections.
Structuring multi-file libraries using import, aliasing (as), and __name__ == '__main__' guards.
Functions & Modular Programming