Writing Reusable Code with Functions & Modules
Up to now, every program you’ve written runs top to bottom, once. Functions change that — they let you name a block of code and call it as many times as you need. Modules let you bring in code that other people have already written and tested. Together, these two ideas are what turn a script into a proper, maintainable program.
Defining Functions — The def Keyword
A function is a named block of code that performs a specific task. You define it once with the def keyword, and then call it from anywhere in your program — as many times as you need. This is the most important tool for avoiding repetition in code.
Think about the log-line parsing you did in M2. You wrote that logic once, inside a single script. But what if you need to parse log lines in five different scripts? Without functions, you’d copy and paste the same code five times. When the log format changes, you’d need to update it in five places. With a function, you update it once and every script immediately benefits.
Every function has a name (snake_case, verb-based — check_health, not health), zero or more parameters, an optional docstring, a body, and an optional return statement. Functions without a return statement implicitly return None.
Parameters — How Functions Receive Data
Parameters are the inputs your function expects. Python gives you several ways to define them.
Positional parameters — the caller must provide them in order. def greet(name, role) expects exactly two arguments.
Default parameters have a fallback used when the caller doesn’t provide that argument: def connect(host, port=5432). Defaults must come after required parameters.
Keyword arguments let the caller name the arguments at the call site, making order irrelevant: connect(port=1521, host=”ora-prod”).
*args collects any number of extra positional arguments into a tuple. **kwargs collects any number of keyword arguments into a dictionary. You’ll see both in library code constantly.
Return Values — Getting Data Back Out
A function’s return statement does two things: sends a value back to the caller, and immediately exits the function. Anything after a return in the same code path never executes.
Functions can return any Python value — a number, string, list, dictionary, boolean, even another function. When a function returns multiple values, Python packs them into a tuple automatically, and you can unpack them at the call site: status, score = analyse_server(server). This is a very common and clean Python pattern.
Scope — Where Variables Live
Scope determines which parts of your program can see a given variable. Variables created inside a function exist only inside that function — they’re local. Variables created outside all functions exist in the global scope and can be read from anywhere.
global keyword — but prefer passing values in and returning them out instead.Lambda Functions — One-Line Anonymous Functions
A lambda is a tiny, anonymous function written on a single line. It takes arguments and returns the result of a single expression. You can’t put loops or multiple statements in a lambda — it’s for very short, simple logic only.
The most common corporate use case is sorting: pass a lambda as the key argument to sorted() or .sort() to sort a list of dictionaries by a specific field. You’ll also see lambdas with map() and filter(). If a lambda starts getting complicated, write a proper def function instead.
Importing Modules — Python’s Standard Library
Python ships with hundreds of built-in modules. These are all installed with Python — no pip needed. You bring a module’s tools into your script using import: the whole module (import os), specific names (from datetime import date), or with an alias (import datetime as dt).
Defining and calling functions
# Basic function — no parameters, no return value
def print_divider():
"""Print a standard divider line."""
print("-" * 48)
print_divider()
# Function with required parameters and return value
def format_ticket(ticket_id, priority, summary):
"""Return a formatted ticket string."""
return f"[{priority.upper()}] {ticket_id}: {summary}"
line = format_ticket("TKT-1041", "high", "Email not loading")
print(line)
# [HIGH] TKT-1041: Email not loading
# Default parameters
def is_critical(value, threshold=90):
"""Return True if value exceeds threshold."""
return value > threshold
is_critical(95) # True — uses default 90
is_critical(95, threshold=80) # True — custom threshold
is_critical(75) # False
# Returning multiple values (Python packs them as a tuple)
def analyse_server(name, cpu, mem, disk):
"""Return server status and worst metric."""
worst = max(cpu, mem, disk)
if worst >= 90: status = "critical"
elif worst >= 75: status = "warning"
else: status = "healthy"
return status, worst
status, peak = analyse_server("db-01", 82, 91, 55)
print(f"Status: {status}, Peak: {peak}%")
# Status: critical, Peak: 91%
*args and **kwargs
# *args — any number of positional arguments (tuple inside)
def total_tickets(*counts):
return sum(counts)
total_tickets(12, 8) # 20
total_tickets(12, 8, 5, 19) # 44
# **kwargs — any number of keyword arguments (dict inside)
def create_ticket(**fields):
print("New ticket:")
for key, val in fields.items():
print(f" {key}: {val}")
create_ticket(title="VPN down", priority="high", assignee="Priya")
Scope — local vs global
MAX_RETRIES = 5 # global
def connect(host):
attempt = 1 # local — destroyed when function returns
print(f"Max retries: {MAX_RETRIES}") # can READ global
# To modify a global — use 'global' keyword (prefer returning values instead)
retry_count = 0
def increment_retries():
global retry_count
retry_count += 1
# Cleaner pattern: pass in, return out
def increment(count):
return count + 1
retry_count = increment(retry_count) # explicit, testable
Lambda functions
# Syntax: lambda arguments: expression
double = lambda x: x * 2
double(5) # 10
fleet = [
{"name": "web-01", "cpu": 45},
{"name": "db-01", "cpu": 82},
{"name": "api-01", "cpu": 33},
]
# Sort by cpu descending
by_cpu = sorted(fleet, key=lambda s: s["cpu"], reverse=True)
# Filter: only high-cpu servers
high = list(filter(lambda s: s["cpu"] > 50, fleet))
# Extract one field (list comprehension is usually cleaner)
cpu_vals = [s["cpu"] for s in fleet] # [45, 82, 33]
Key standard library modules
import os, sys, math, random
from datetime import date, datetime, timedelta
# ── os ──
os.getcwd() # current directory
os.listdir(".") # list files
os.path.exists("config.txt") # True / False
os.path.join("logs", "app.log") # OS-safe path
os.environ.get("DB_HOST", "localhost") # env variable with default
os.makedirs("output", exist_ok=True) # create dirs safely
# ── sys ──
sys.version # Python version string
sys.argv # command-line arguments list
sys.exit(0) # exit with success (0) or error (1)
# ── datetime ──
today = date.today() # 2026-06-14
now = datetime.now() # 2026-06-14 09:33:12
tomorrow = today + timedelta(days=1) # 2026-06-15
formatted = now.strftime("%d-%b-%Y") # "14-Jun-2026"
parsed = datetime.strptime("14-06-2026", "%d-%m-%Y")
# ── math ──
math.ceil(4.2) # 5 — always round up
math.floor(4.9) # 4 — always round down
math.sqrt(144) # 12.0
math.log(1000, 10) # 3.0 — log base 10
# ── random ──
random.randint(1, 100) # random integer 1–100
random.choice(["a", "b", "c"]) # pick one at random
random.shuffle(my_list) # shuffle in place
random.sample(my_list, 3) # 3 unique random items
Docstrings — documenting your functions
def calculate_sla_status(age_hours, priority):
"""
Determine SLA compliance status for a support ticket.
Args:
age_hours (int): How long the ticket has been open, in hours.
priority (str): 'Critical', 'High', 'Medium', or 'Low'.
Returns:
str: One of 'OK', 'AT RISK', or 'BREACHED'.
"""
limits = {"Critical": 1, "High": 8, "Medium": 24, "Low": 72}
limit = limits.get(priority, 24)
pct = (age_hours / limit) * 100
if age_hours > limit: return "BREACHED"
elif pct >= 80: return "AT RISK"
else: return "OK"
help(calculate_sla_status) # Python reads the docstring automatically
Notice in each example how the main logic at the bottom becomes short and readable because the heavy lifting is inside named functions. The main block reads almost like a checklist — that’s the goal.
Small, focused functions that can be reused across any IT support script. The main code at the bottom is just function calls — easy to read, easy to change, each piece independently testable.
from datetime import datetime
def format_ticket_id(num):
"""Return zero-padded ticket ID e.g. TKT-00041."""
return f"TKT-{num:05d}"
def get_sla_hours(priority):
"""Return SLA time limit in hours for given priority."""
limits = {"Critical": 1, "High": 8, "Medium": 24, "Low": 72}
return limits.get(priority, 24)
def sla_status(age_hours, priority):
"""Return SLA status string and percentage used."""
limit = get_sla_hours(priority)
pct = (age_hours / limit) * 100
if age_hours > limit: return "✗ BREACHED", pct
elif pct >= 80: return "⚠ AT RISK", pct
else: return "✓ OK", pct
def print_ticket_row(num, priority, summary, age_hours):
"""Print one formatted ticket row."""
tid = format_ticket_id(num)
status, pct = sla_status(age_hours, priority)
print(f"{tid} {priority:10} {age_hours:>3}h {pct:>5.0f}% {status:12} {summary}")
# ── Main ──
tickets = [
(1041, "High", "Email not loading", 5),
(1035, "Critical", "Production DB down", 2),
(1029, "Medium", "Printer offline", 26),
(1018, "Low", "Request new keyboard", 48),
]
print(f"{'Ticket':10} {'Priority':10} {'Age':>4} {'SLA%':>6} {'Status':13} Summary")
print("-" * 72)
for t in tickets:
print_ticket_row(*t) # * unpacks the tuple into positional args
————————————————————————
TKT-01041 High 5h 63% ✓ OK Email not loading
TKT-01035 Critical 2h 200% ✗ BREACHED Production DB down
TKT-01029 Medium 26h 108% ✗ BREACHED Printer offline
TKT-01018 Low 48h 67% ✓ OK Request new keyboard
Functions that build SQL query strings from parameters — a safe, reusable pattern. Shows how functions with defaults and **kwargs make complex operations readable and maintainable.
def build_select(table, columns="*", where=None, order_by=None, limit=None):
"""Build a SELECT statement from components."""
if isinstance(columns, list):
columns = ", ".join(columns)
query = f"SELECT {columns} FROM {table}"
if where: query += f" WHERE {where}"
if order_by: query += f" ORDER BY {order_by}"
if limit: query += f" FETCH FIRST {limit} ROWS ONLY"
return query
def build_insert(table, **data):
"""Build an INSERT statement from keyword arguments."""
cols = ", ".join(data.keys())
vals = ", ".join([f"'{v}'" if isinstance(v, str) else str(v)
for v in data.values()])
return f"INSERT INTO {table} ({cols}) VALUES ({vals})"
def preview_query(query, label="Query"):
"""Print a query with a label for review."""
print(f"\n── {label} ──\n {query};")
# ── Main ──
preview_query(
build_select("employees",
columns=["emp_id", "name", "salary"],
where="dept = 'IT' AND active = 1",
order_by="salary DESC",
limit=10),
"IT salary report"
)
preview_query(
build_insert("audit_log",
event="LOGIN", user_id=1042, status="SUCCESS"),
"Audit insert"
)
SELECT emp_id, name, salary FROM employees WHERE dept = ‘IT’ AND active = 1 ORDER BY salary DESC FETCH FIRST 10 ROWS ONLY;
── Audit insert ──
INSERT INTO audit_log (event, user_id, status) VALUES (‘LOGIN’, 1042, ‘SUCCESS’);
Uses os.environ to read environment variables with safe defaults, exposed through a clean get_config() function. This is exactly how real DevOps configuration loading is structured.
import os
from datetime import datetime
def get_env(key, default=None, required=False):
"""Read an environment variable safely."""
value = os.environ.get(key, default)
if required and value is None:
raise EnvironmentError(f"Required env var '{key}' is not set.")
return value
def get_config():
"""Build application configuration from environment variables."""
return {
"env": get_env("APP_ENV", default="development"),
"db_host": get_env("DB_HOST", default="localhost"),
"db_port": int(get_env("DB_PORT", default="5432")),
"log_level": get_env("LOG_LEVEL", default="INFO").upper(),
"max_conn": int(get_env("MAX_CONN", default="10")),
}
def print_config(cfg):
"""Print config in a readable format."""
sensitive = {"db_password", "api_key"}
ts = datetime.now().strftime("%H:%M:%S")
print(f"Configuration loaded at {ts}\n")
for key, value in cfg.items():
display = "***" if key in sensitive else value
print(f" {key:14}: {display}")
# ── Main ──
config = get_config()
print_config(config)
if config["env"] == "production":
print("\n⚠ Production environment — handle with care.")
env : development
db_host : localhost
db_port : 5432
log_level : INFO
max_conn : 10
A data cleaning pipeline built from small, composable functions — each doing one thing. A run_pipeline() function chains them. This reflects exactly how ML engineers structure preprocessing before feeding data to a model.
import math
def remove_nulls(records, required_keys):
"""Remove records missing any required key."""
return [r for r in records
if all(r.get(k) is not None for k in required_keys)]
def clamp(records, field, min_val, max_val):
"""Cap field values to [min_val, max_val]."""
for r in records:
r[field] = max(min_val, min(max_val, r[field]))
return records
def normalise(records, field):
"""Min-max normalise a numeric field to 0.0–1.0."""
values = [r[field] for r in records]
lo, hi = min(values), max(values)
rng = hi - lo or 1
for r in records:
r[f"{field}_norm"] = round((r[field] - lo) / rng, 4)
return records
def add_log_feature(records, field):
"""Add log1p transform of field as a new feature."""
for r in records:
r[f"{field}_log"] = round(math.log1p(r[field]), 4)
return records
def run_pipeline(raw):
"""Run the full preprocessing pipeline."""
data = remove_nulls(raw, ["age", "salary"])
data = clamp(data, "age", 18, 65)
data = normalise(data, "salary")
data = add_log_feature(data, "salary")
return data
# ── Main ──
raw = [
{"id": 1, "age": 28, "salary": 55000},
{"id": 2, "age": 72, "salary": 92000}, # age clamped to 65
{"id": 3, "age": None, "salary": 48000}, # removed — missing age
{"id": 4, "age": 35, "salary": 78000},
]
clean = run_pipeline(raw)
print(f"{'ID':>3} {'Age':>5} {'Salary':>8} {'Norm':>7} {'Log':>8}")
print("-" * 36)
for r in clean:
print(f"{r['id']:>3} {r['age']:>5} {r['salary']:>8,} {r['salary_norm']:>7.4f} {r['salary_log']:>8.4f}")
————————————
1 28 55,000 0.0000 10.9152
2 65 92,000 1.0000 11.4297
4 35 78,000 0.6216 11.2647
A complete automation script structured entirely around functions. Uses datetime for timestamps, os for paths, and lambda for sorting. The main block reads almost like a specification — which is exactly what well-structured function-based code should feel like.
import os
from datetime import datetime
def get_report_filename(name, ext="txt"):
"""Generate a timestamped filename."""
ts = datetime.now().strftime("%Y%m%d_%H%M")
safe = name.lower().replace(" ", "_")
return f"{safe}_{ts}.{ext}"
def ensure_output_dir(directory="reports"):
"""Create output directory if it doesn't exist."""
os.makedirs(directory, exist_ok=True)
return directory
def summarise_metrics(records, value_key, label_key):
"""Return summary stats for a list of records."""
values = [r[value_key] for r in records]
return {
"count": len(values),
"total": sum(values),
"average": round(sum(values) / len(values), 1),
"highest": max(records, key=lambda r: r[value_key])[label_key],
"lowest": min(records, key=lambda r: r[value_key])[label_key],
}
def build_report_text(title, stats, generated_at):
"""Format report as a text block."""
lines = [
f"{'='*50}", f" {title}", f" Generated: {generated_at}", f"{'='*50}",
f" Records : {stats['count']}",
f" Total : {stats['total']:,}",
f" Average : {stats['average']:,}",
f" Top : {stats['highest']}",
f" Bottom : {stats['lowest']}",
f"{'='*50}",
]
return "\n".join(lines)
# ── Main ──
sales = [
{"region": "North", "revenue": 1_240_000},
{"region": "South", "revenue": 980_000},
{"region": "East", "revenue": 1_560_000},
{"region": "West", "revenue": 820_000},
]
stats = summarise_metrics(sales, "revenue", "region")
report = build_report_text("Regional Revenue Report", stats,
datetime.now().strftime("%d %b %Y %H:%M"))
print(report)
Regional Revenue Report
Generated: 14 Jun 2026 09:33
==================================================
Records : 4
Total : 4,600,000
Average : 1,150,000.0
Top : East
Bottom : West
==================================================
Before writing any code, ask yourself: what are the inputs, what should it return, and what should it be called? Getting those three things right is 80% of writing a good function.
servers = [{"name":"web","cpu":45},{"name":"db","cpu":92},{"name":"api","cpu":70}]
total = 0
for s in servers: total += s["cpu"]
avg = total / len(servers)
for s in servers:
if s["cpu"] > 80: print(f"ALERT: {s['name']} cpu={s['cpu']}%")
print(f"Average CPU: {avg:.1f}%")
📋 Server Monitoring Toolkit
Build monitor_toolkit.py structured entirely around reusable functions. Every piece of logic must live inside a named function — the main block should only call functions and print results.
- calculate_health_score(cpu, mem, disk) — returns a score 0–100. Deduct: 1 point per % CPU above 60, 1 point per % memory above 70, 2 points per % disk above 80. Minimum 0.
- classify_server(score) — returns a tuple of status string and emoji: “Healthy” ≥80, “Degraded” ≥55, “Critical” <55.
- generate_server_report(servers) — accepts a list of server dicts (name, env, cpu, mem, disk). Calls the above two functions for each, returns enriched list with score and status added.
- summary_by_environment(report) — groups by env, returns dict showing server count, average score, and status category counts per environment.
- print_report(report) and print_summary(summary) — two separate print functions with aligned f-string column formatting.
- Use from datetime import datetime and import os genuinely: timestamp in the report header, and read a MONITOR_ENV env variable (default “local”) displayed in the header.
These questions test how functions behave — parameters, scope, return values, and imports. Some require you to trace through code mentally before answering.
def greet(name, role="Engineer"):
return f"Hello, {name}. Welcome, {role}."
def get_status(cpu, threshold=75):
if cpu >= threshold:
return "alert"
return "ok"
a = get_status(80)
b = get_status(80, threshold=90)
print(a, b)
