Python Hands-On Labs

Choose a lab from the contents. Complete one lab at a time, then use Previous Lesson or Next Lesson to continue.

Start with the file-processing lab after fundamentals. Take the production scripting bridge before the EC2 lab, then practice retries and request pacing. Finish with the inventory capstone.

Each lab provides prerequisites, input, a task, expected output, hints, an expandable solution, checks, and a further challenge. Try the task before opening its solution.

After these labs, explore cloud scripts, production scenarios, MCQs, and interview questions.

Python File Processing Lab

Prerequisites

Complete loops, functions, exceptions, and files in fundamentals. Use Python 3; no packages or cloud account are required. Work in a scratch directory for input and output files.

Sample input

Save as servers.csv:

name,server,status
web-01,prod,healthy
web-02,dev,warning
web-03,prod,critical

Task

Create filter_servers.py to read the file, keep critical records, and write critical-servers.csv. Validate headers, process one row at a time, and print the count. Invalid or missing input must produce an error and nonzero exit code. Do not replace an existing report without removing it first.

Run python filter_servers.py.

Expected output

The terminal prints Found 1 critical servers. and the file contains:

name,server,status
web-03,prod,critical
HintUse DictReader and DictWriter with explicit encoding and newline handling. Validate fieldnames before creating the output file.
Show solution
import csv
import sys
from pathlib import Path


def make_report(source, destination):
    if source.resolve() == destination.resolve():
        raise ValueError("Input and output must be different files")
    fields = ["name", "server", "status"]
    count = 0
    created = False
    try:
        with source.open(encoding="utf-8", newline="") as src:
            reader = csv.DictReader(src, strict=True)
            if reader.fieldnames != fields:
                raise ValueError("Expected headers: name,server,status")
            with destination.open("x", encoding="utf-8", newline="") as dst:
                created = True
                writer = csv.DictWriter(dst, fieldnames=fields)
                writer.writeheader()
                for row in reader:
                    if None in row or any(row[key] is None for key in fields):
                        raise ValueError("Unexpected number of fields")
                    if row["status"] == "critical":
                        writer.writerow(row)
                        count += 1
    except (OSError, ValueError, csv.Error, UnicodeError):
        if created:
            destination.unlink(missing_ok=True)
        raise
    return count


def main():
    try:
        count = make_report(Path("servers.csv"), Path("critical-servers.csv"))
    except (OSError, ValueError, csv.Error, UnicodeError) as error:
        print(f"Report failed: {error}", file=sys.stderr)
        return 1
    print(f"Found {count} critical servers.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Check your work

Use a fresh output path or remove the generated report between successful runs.

InputExpected behavior
Sample aboveOne critical record, exit 0.
Header onlyHeader-only output, count 0.
Missing fileError on stderr, exit 1, no report.
Wrong headers or incomplete rowError, exit 1, no partial report.
Existing outputError; existing output unchanged.

Inspect $LASTEXITCODE in PowerShell or echo $? in bash immediately after the command.

Knowledge check and challenge

Why stream records? Memory usage stays bounded by the current record instead of growing with the whole file.

Add input, output, and status flags using the production scripting bridge. Test a quoted comma in a name and a malformed row. Explain whether a scheduled job should replace or retain its previous report.

From Python Basics to Production Scripts

Prerequisites

Complete functions, exceptions, files, and modules in fundamentals, then the file-processing lab. This bridge runs locally without AWS credentials.

Set up a project environment

Create a project directory and run python -m venv .venv. Activate it with .venv\Scripts\Activate.ps1 in PowerShell or source .venv/bin/activate in bash. Alternatively, call .venv\Scripts\python.exe on Windows or .venv/bin/python on Unix directly. Confirm python -c "import sys; print(sys.executable)" points inside the environment.

For the later AWS labs, install boto3 with python -m pip install boto3. Record tested dependencies with python -m pip freeze > requirements.txt, and recreate them with python -m pip install -r requirements.txt. Keep .venv/ out of version control. See the Python virtual environment tutorial.

Sample input and task

Turn a list of server states into a command-line report. Save the solution as report.py. Accept a status filter, log a count to stderr, print matching names to stdout, and return zero on success.

Run python report.py --status critical. Expected stdout: web-02. Stderr includes INFO matched=1. An unsupported status must produce an argument error and a nonzero exit code.

HintKeep filtering separate from argument parsing and logging so tests can pass records directly.
Show solution: report.py
import argparse
import logging
import os


def select_names(records, status):
    return [row["name"] for row in records if row["status"] == status]


def main():
    parser = argparse.ArgumentParser(description="Filter server states")
    parser.add_argument("--status", choices=["healthy", "critical"],
                        default="critical")
    args = parser.parse_args()
    level = os.environ.get("REPORT_LOG_LEVEL", "INFO").upper()
    if level not in {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}:
        parser.error("REPORT_LOG_LEVEL must be a logging level")
    logging.basicConfig(level=level, format="%(levelname)s %(message)s")
    records = [{"name": "web-01", "status": "healthy"},
               {"name": "web-02", "status": "critical"}]
    names = select_names(records, args.status)
    logging.info("matched=%d", len(names))
    for name in names:
        print(name)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Test the behavior

Save this beside the script as test_report.py and run python -m unittest -v.

import unittest
from report import select_names


class ReportTests(unittest.TestCase):
    def test_filters_without_changing_input(self):
        rows = [{"name": "web-01", "status": "healthy"},
                {"name": "web-02", "status": "critical"}]
        self.assertEqual(select_names(rows, "critical"), ["web-02"])
        self.assertEqual(len(rows), 2)

    def test_empty_and_no_match(self):
        self.assertEqual(select_names([], "critical"), [])
        self.assertEqual(select_names(
            [{"name": "web-01", "status": "healthy"}], "critical"), [])

Before adding an API

ConcernApply it to your script
ConfigurationUse CLI flags for the current run and environment variables for deployment defaults; validate both.
CredentialsUse the SDK credential chain and a role or configured profile.
LoggingSend diagnostics to stderr and report data to stdout or a file; exclude secrets.
TimeoutsBound connection and read waits; a read timeout is not a deadline for a whole paginated job.
RetriesUse the SDK’s bounded retry policy for supported transient failures.
Exit codesReturn zero only on success so CI and schedulers can detect failures.

For boto3, use Config(connect_timeout=5, read_timeout=20, retries={"mode": "standard", "total_max_attempts": 4}). The attempt limit includes the original request. See the SDK retry documentation.

Completion checklist and challenge

  • Run both status filters and the invalid-status case.
  • Pass the unit tests without calling a network service.
  • Explain the difference between logs and report output.
  • Add a CSV input argument and a test for a missing required column.

Continue to the EC2 report lab and then the inventory capstone.

Python boto3 EC2 Report Lab

Prerequisites

Complete the production scripting bridge. This exercise initially uses only Python. The optional live extension needs boto3, configured AWS credentials, a region, and ec2:DescribeInstances permission.

Sample input

Use this simulated API page; no instance needs to be created:

sample = {"Reservations": [{"Instances": [
    {"InstanceId": "i-example1", "State": {"Name": "running"},
     "InstanceType": "t3.micro", "Tags": [{"Key": "Name", "Value": "web-01"}]},
    {"InstanceId": "i-example2", "State": {"Name": "stopped"},
     "InstanceType": "t3.micro"}
]}]}

Task and expected output

Write rows_from_pages(pages, tag_key=None) to process every page, return normalized rows sorted by state and ID, support a tag-key filter, and use unnamed when the Name tag is absent.

running | i-example1 | web-01 | t3.micro
stopped | i-example2 | unnamed | t3.micro

With tag_key="Name", only the first row remains.

HintIterate through pages, reservations, and instances. Convert tags into a dictionary and keep client creation separate from transformation.
Show solution
def rows_from_pages(pages, tag_key=None):
    rows = []
    for page in pages:
        for reservation in page.get("Reservations", []):
            for instance in reservation.get("Instances", []):
                tags = {tag["Key"]: tag["Value"]
                        for tag in instance.get("Tags", [])}
                if tag_key is not None and tag_key not in tags:
                    continue
                rows.append({
                    "id": instance["InstanceId"],
                    "state": instance["State"]["Name"],
                    "type": instance["InstanceType"],
                    "name": tags.get("Name", "unnamed"),
                })
    return sorted(rows, key=lambda row: (row["state"], row["id"]))


# Put the sample definition above this call.
for row in rows_from_pages([sample]):
    print(f"{row['state']} | {row['id']} | {row['name']} | {row['type']}")

Check your work

  • Empty Reservations returns no rows.
  • Splitting the sample across two pages still returns both instances.
  • Filtering by an absent tag returns no rows.
  • Reversing page order does not change report order.
  • Missing tags do not raise an exception.

Optional live extension

Pass client.get_paginator("describe_instances").paginate() to your function. Configure timeouts and bounded SDK retries as shown in the bridge. A single API response may not cover the fleet; see the DescribeInstances paginator.

Live output depends on your account and region. Credential and permission errors are failures, not empty inventories. The capstone includes a complete CLI and error handling.

Challenge

Filter running instances missing an Owner tag. Explain the difference between a missing tag and a present tag with an empty value. Export rows as CSV and test against the local sample.

Python Retry and Logging Lab

Prerequisites

Know functions, exceptions, and logging from the production scripting bridge. This simulation uses only Python and makes no network calls.

Sample input and task

A fake service raises ConnectionError twice, then returns ok. Allow four total attempts, wait 0.1 then 0.2 seconds between the first three attempts, and log each failure. Propagate unrelated errors immediately.

Expected output

Warnings indicate attempts 1 and 2 failed, then stdout prints ok. No sleep follows success or the last failure. Four total attempts means one original call and at most three retries.

HintPass the operation and sleep function as arguments. Catch only the simulated transient failure and re-raise when the attempt budget is exhausted.
Show solution
import logging
import time


def run_with_retry(operation, max_attempts=4, base_delay=0.1, sleep=time.sleep):
    if max_attempts < 1 or base_delay < 0:
        raise ValueError("Invalid retry configuration")
    for attempt in range(1, max_attempts + 1):
        try:
            return operation()
        except ConnectionError:
            if attempt == max_attempts:
                logging.error("attempt=%d exhausted=true", attempt)
                raise
            delay = min(base_delay * 2 ** (attempt - 1), 5.0)
            logging.warning("attempt=%d retry_in=%.2f", attempt, delay)
            sleep(delay)


def main():
    logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
    outcomes = iter([ConnectionError("temporary"), ConnectionError("temporary"), "ok"])

    def fake_call():
        result = next(outcomes)
        if isinstance(result, Exception):
            raise result
        return result

    print(run_with_retry(fake_call))


if __name__ == "__main__":
    main()

Check your work

Replace sleep with delays.append, where delays = [], to test without waiting.

OperationExpected result
Immediate successOne call; no sleeps.
Two failures then successThree calls; delays [0.1, 0.2].
Always ConnectionErrorFour calls; three sleeps; exception propagates.
ValueErrorOne call; no sleeps; exception propagates.
Zero attempt budgetConfiguration error before any call.

Challenge and production connection

Add bounded jitter and test delay ranges instead of exact values. For a real service, use the client’s exception types and timeouts. Retrying writes requires an idempotency strategy.

For boto3, use SDK retries instead of wrapping them in this simulation: nested policies multiply attempts. The sample logs key/value text; extend it to JSON if your log collector requires JSON events.

Continue to request pacing.

Python Rate Limit Safety Lab

Prerequisites

Complete the retry lab. This simulation uses Python’s standard library in a single process.

Sample input and task

Send 50 simulated requests at no more than 10 starts per second by leaving at least 0.1 seconds between starts. Use a monotonic clock so wall-clock adjustments cannot change the pacing.

Expected output

Print Sent 50 requests. The final start occurs at least 4.9 seconds after the first. Real scheduling may be slower. Do not sleep after the last request.

HintTrack the next permitted start. A slow request must not cause a catch-up burst. Validate the rate before computing its reciprocal.
Show solution
import math
import time


def paced_calls(send, count, rate, clock=time.monotonic, sleep=time.sleep):
    if count < 0 or not math.isfinite(rate) or rate <= 0:
        raise ValueError("Count must be nonnegative and rate positive and finite")
    interval = 1.0 / rate
    next_start = clock()
    for number in range(count):
        while True:
            delay = next_start - clock()
            if delay <= 0:
                break
            sleep(delay)
        next_start = clock() + interval
        send(number)


if __name__ == "__main__":
    sent = []
    paced_calls(sent.append, count=50, rate=10)
    print(f"Sent {len(sent)} requests")

Check your work

Use a fake clock to avoid waiting during tests:

now = [0.0]
starts = []


def clock():
    return now[0]


def sleep(seconds):
    now[0] += seconds


paced_calls(lambda number: starts.append(clock()), 50, 10, clock, sleep)
assert len(starts) == 50
assert starts[-1] >= 4.9 - 1e-9
assert all(b - a >= 0.1 - 1e-9 for a, b in zip(starts, starts[1:]))

Also test zero requests, invalid rates, and a send function that advances time by 0.3 seconds. Slow calls must not cause a catch-up burst.

Knowledge check and challenge

Does this enforce a shared limit across workers? No. Each process has its own schedule. Shared quotas need coordination.

Extend the simulation with a rate-limit response that specifies a retry delay. Respect that delay. Real APIs can use fixed windows, sliding windows, or token buckets; validate the service policy instead of treating this example as a universal limiter.

Continue to the inventory capstone.

Capstone: AWS Inventory Reporter

Prerequisites

Complete the production scripting bridge, EC2 reporting, and retry labs. The offline project needs Python 3 and no third-party packages. Optional live mode needs boto3, configured credentials, and ec2:DescribeInstances permission in your chosen region.

Project brief

An operations team needs a repeatable EC2 inventory report. Build a command-line tool that:

  1. Reads either local response fixtures or all EC2 response pages in one region.
  2. Produces a deterministic CSV with region, ID, state, type, and name.
  3. Handles missing Name tags and empty inventories.
  4. Uses connection/read timeouts and at most four SDK attempts per request.
  5. Logs a count and region, and returns a nonzero exit code on failure.
  6. Refuses to overwrite an existing report and removes a partial report if writing fails.
  7. Can be tested without credentials or network calls.

Sample input and downloads

Save these files in the same directory. The IDs in the fixture are fictional.

Write your own inventory.py before opening the reference implementation below.

Expected output

python inventory.py --fixture sample-pages.json --region us-east-1 --output inventory.csv

Stdout: Wrote 2 instances to inventory.csv. The file contains:

region,id,state,type,name
us-east-1,i-example1,running,t3.micro,web-01
us-east-1,i-example2,stopped,t3.micro,unnamed

The region flag labels offline fixture records; fixtures themselves contain no region metadata. A second run using the same output path must fail without changing the first report. Use a new path for the next successful run.

Hint: break the project into four partsSeparate rows_from_pages(pages, region), collect_live(client, region), write_report(rows, output), and main(argv=None). Inject a fake client into the collection function. Import boto3 only in live mode so offline use needs no SDK installation.
Show the reference solution

Download inventory.py. It uses the SDK paginator, bounded standard retries, explicit timeouts, a pure normalization function, and exclusive output-file creation. It makes no AWS calls unless you explicitly pass --live.

The report currently collects and sorts all rows in memory. For a very large fleet, choose a streaming or external-sort design. Pagination alone does not bound report memory usage.

Run the tests

python -m unittest -v test_inventory.py

Tests cover multiple pages, missing tags, stable ordering, empty output, malformed input, output preservation, partial-write cleanup, and propagated pagination errors. A mocked paginator exercises the collection contract; it does not simulate the SDK retry engine.

Optional live run

Install boto3 in your virtual environment and use your configured AWS profile or role:

python -m pip install boto3
python inventory.py --live --region us-east-1 --output live-inventory.csv

Add --profile training if you use a named profile. This mode reads EC2 inventory and does not create or modify AWS resources. Permission or credential failures must fail the run; they must not become a successful empty report.

The SDK handles supported transient errors within the configured attempt budget. See SDK retries and the EC2 paginator. A whole-job deadline, distributed rate limits, and multi-account credentials are outside this first version.

Completion checklist

  • Generate the exact sample report and pass the offline tests.
  • Explain why only reading the first response page loses data.
  • Demonstrate a failed run leaves no new report.
  • Explain timeouts, total attempts, and failure exit codes.
  • Give a five-minute walkthrough of your design and one limitation.

Further challenges

Add multiple regions, a missing-Owner-tag report, and a scheduled CI job that publishes the CSV as an artifact. Test one failed region and decide whether partial results should count as success. If users open reports in spreadsheet software, add and test a policy for untrusted tag values that resemble formulas.

Continue to production scenarios or the interview revision route.

5.18 Hands-on Exercises

Practice Programs

  • Write a function that takes a list of mixed types and returns counts of how many are int, str, and float.
  • Given a list with duplicate IP addresses, return only the unique ones using a set.
  • Write a function demonstrating the mutable-default-argument bug, then fix it.
  • Parse a small JSON config string and print the type of each top-level value.
  • Write a script that shows id() staying the same after list.append() but changing after tuple concatenation.

Mini Projects

  • Config loader — read a dict of settings, validate each value’s type with isinstance(), and raise a clear error for any mismatch.
  • IP-address deduplicator — read a list of IPs from a (simulated) log file and report unique vs total counts using a set.
  • Type profiler — given any object, print its type, id, and whether it’s mutable or immutable (based on a lookup table of known types).

Quick Interview Answer

“These exercises reinforce the chapter’s core ideas hands-on: counting types in a mixed list exercises type()/isinstance(), deduplicating IPs exercises set, the mutable-default-argument fix exercises function default semantics, and the type profiler exercises identity (id()) plus the mutable/immutable distinction from 5.9 Mutable vs Immutable Types together.”

Common Mistakes

  • Skipping the mutable-default exercise as “just theory” — it’s one of the most common real bugs in production Python code.
  • Building the type profiler with a hardcoded if/elif chain per type instead of a lookup table, making it harder to extend.

6.15 Hands-on Exercises

Practice Programs

  • Write a function demonstrating UnboundLocalError, then fix it using the global keyword.
  • Given a nested list, show the difference between copy.copy() and copy.deepcopy() by mutating a nested element.
  • Write a closure using nonlocal to implement a simple counter.

Mini Projects

  • Configuration loader — build a small config loader that reads settings from environment variables with sensible defaults, using 6.11 Variables in DevOps as a starting point.
  • Reference tracer — build a tool that takes any object and prints its id(), type(), and sys.getrefcount() before and after a mutation, to make the concepts from 6.2 Objects and Variable References and 6.6 Garbage Collection concrete.

Quick Interview Answer

“These exercises reinforce the chapter’s core ideas hands-on: the UnboundLocalError-then-fix exercise exercises scope and the global keyword; the shallow-vs-deep-copy exercise exercises reference sharing on nested mutable objects; the nonlocal counter exercises closures and enclosing scope; and the reference tracer ties identity, type, and reference counting together into one small diagnostic tool.”

Common Mistakes

  • Skipping the shallow-vs-deep-copy exercise as “just theory” — it’s one of the most common real bugs when working with nested config dicts or lists of records.
  • Building the reference tracer without accounting for the temporary reference sys.getrefcount()’s own call creates, and misreading the count as a result.

7.19 Hands-on Exercises & Mini Projects

CPU Usage Calculator

Build a function that takes total CPU time used and elapsed time, and returns the percentage utilization using arithmetic operators.

def cpu_utilization(used_seconds, elapsed_seconds):
    return (used_seconds / elapsed_seconds) * 100

>>> cpu_utilization(45, 60)
75.0

Disk Usage Alert

Build a function that returns an alert message using comparison operators against a configurable threshold.

def check_disk(percent_used, threshold=90):
    return "ALERT" if percent_used > threshold else "OK"

>>> check_disk(95)
'ALERT'

Status Code Validator

Build a function using chained comparisons to classify any HTTP status code into its category.

def classify_status(code):
    if 200 <= code < 300:
        return "Success"
    elif 300 <= code < 400:
        return "Redirect"
    elif 400 <= code < 500:
        return "Client Error"
    else:
        return "Server Error"

>>> classify_status(301)
'Redirect'

Service Health Checker

Build a function combining logical operators to determine overall service health from multiple boolean signals.

def is_healthy(is_running, response_time_ms, error_rate):
    return is_running and response_time_ms < 500 and error_rate < 0.05

>>> is_healthy(True, 230, 0.01)
True
>>> is_healthy(True, 800, 0.01)
False

Quick Interview Answer

“These exercises reinforce the chapter’s core ideas hands-on: the CPU calculator exercises arithmetic (/ and *), the disk alert exercises comparison plus the ternary operator, the status validator exercises chained comparisons across an if/elif ladder, and the health checker exercises and-chained logical operators with short-circuit evaluation skipping later checks once an earlier one already fails.”

Common Mistakes

  • Using / where // was intended (or vice versa) in the CPU calculator, producing a fractional result where a whole number was expected, or a truncated one where precision mattered.
  • Writing the status validator as separate if statements instead of if/elif, letting multiple branches match and return inconsistent results.
  • Ordering the health checker’s conditions without considering short-circuit performance — putting an expensive check before a cheap one that’s more likely to fail first.

8.17 Hands-on Exercises

User Input Converter

Build a function that safely converts input() text to int, float, or bool based on a requested target type, with sensible error handling.

def convert_input(value, target_type):
    try:
        if target_type == bool:
            return value.strip().lower() in ("true", "yes", "1")
        return target_type(value)
    except (ValueError, TypeError):
        return None

>>> convert_input("42", int)
42
>>> convert_input("yes", bool)
True
>>> convert_input("abc", int)
None

CSV Parser

Build a function that reads CSV rows and converts specified numeric columns, handling any row with bad data gracefully.

import csv, io

def parse_ages(csv_text):
    reader = csv.DictReader(io.StringIO(csv_text))
    result = []
    for row in reader:
        try:
            row["age"] = int(row["age"])
        except ValueError:
            row["age"] = None
        result.append(row)
    return result

>>> parse_ages("name,age\nAlice,30\nBob,N/A")
[{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': None}]

Log Analyzer

Build a function that extracts and converts status codes from log lines, classifying each safely even if a line is malformed.

def classify_status_line(status_str):
    try:
        code = int(status_str)
    except ValueError:
        return "invalid"
    if 200 <= code < 300:
        return "success"
    elif 400 <= code < 500:
        return "client_error"
    elif code >= 500:
        return "server_error"
    return "other"

>>> classify_status_line("404")
'client_error'
>>> classify_status_line("N/A")
'invalid'

Mini Projects

  • Config loader — reads a dict of raw string values and converts each to its declared type (int/float/bool), reporting any that fail.
  • CLI calculator — safely converts two input() values to float and applies a chosen arithmetic operator, handling invalid input without crashing.
  • Environment-variable validator — checks a list of required env vars exist and are convertible to their expected types before a script proceeds.

Quick Interview Answer

“These exercises reinforce the chapter’s core ideas hands-on: the input converter exercises try/except around a dynamic target type plus the boolean-string gotcha, the CSV parser exercises per-row resilience so one bad record doesn’t crash the whole import, and the log analyzer combines safe conversion with chained-comparison classification from 7.12 Chained Comparisons.”

Common Mistakes

  • Letting one malformed CSV row crash the entire parse instead of catching the conversion failure per-row and continuing.
  • Forgetting the boolean branch in convert_input needs its own logic — calling bool(value) directly on a string would always return True for any non-empty input.
  • Not handling the case where target_type itself is invalid or unexpected, instead of assuming callers always pass one of the anticipated types.

9.16 Hands-on Exercises

Log Level Analyzer

Build a function that scans a batch of log lines and tallies how many fall into each severity level — an at-a-glance health summary without opening a log viewer.

import re
from collections import Counter

def analyze_log_levels(lines):
    levels = [re.search(r"\b(INFO|WARNING|ERROR)\b", l).group(1) for l in lines]
    return dict(Counter(levels))

>>> analyze_log_levels([
...     "2026-07-13 10:00:01 INFO Service started",
...     "2026-07-13 10:00:05 ERROR Connection refused",
...     "2026-07-13 10:00:07 WARNING High memory usage",
... ])
{'INFO': 1, 'ERROR': 1, 'WARNING': 1}

IP Address Extractor

Pull every IPv4 address out of free-form text — the first step in building a blocklist, an allowlist audit, or a geo-lookup report from security logs.

>>> text = "Connections from 192.168.1.10 and 10.0.0.5 were blocked; 8.8.8.8 allowed"
>>> re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)
['192.168.1.10', '10.0.0.5', '8.8.8.8']

Configuration File Parser

Read a simple key=value config format, with comment lines starting with # — a recurring task for any tool that needs its own settings file without pulling in a full config-parsing library.

def parse_config(text):
    result = {}
    for line in text.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        key, _, value = line.partition("=")
        result[key] = value
    return result

>>> parse_config("# config\nhost=localhost\nport=8080\n\ndebug=true")
{'host': 'localhost', 'port': '8080', 'debug': 'true'}

Password Strength Checker

Enforce a minimum length plus a mix of character classes — a standard first line of defense against weak passwords during account signup.

def is_strong_password(pw):
    if len(pw) < 8:
        return False
    has_upper = any(c.isupper() for c in pw)
    has_lower = any(c.islower() for c in pw)
    has_digit = any(c.isdigit() for c in pw)
    has_special = any(not c.isalnum() for c in pw)
    return all([has_upper, has_lower, has_digit, has_special])

>>> is_strong_password("Weak1")
False
>>> is_strong_password("Str0ng!Pass")
True

Mini Projects

  • DevOps health report generator — turn raw HTTP status-code counts into a single human-readable error-rate line ("1000 requests, 2.0% error rate"), reusing Counter from earlier in this chapter.
  • CI/CD build summarizer — parse a Jenkins-style console line ("Build #42 SUCCESS in 3m 15s") into a structured {"build", "status", "duration"} result, ready to feed a Slack notification.
  • Kubernetes log analyzer — combine the pod-name parser from 9.12 Strings in DevOps and AWS with the log-level counter above to produce a per-deployment error-rate summary.

Quick Interview Answer

“These exercises tie the chapter’s tools together: the log analyzer combines regex extraction with Counter; the IP extractor is a single well-chosen regex; the config parser leans on partition() instead of a fragile manual split; and the password checker chains several is* validation methods with any(). The common thread is picking the right existing tool — regex, Counter, partition, is* — over hand-rolled character-by-character logic.”

Common Mistakes

  • Using split("=") instead of partition("=") in the config parser — split() breaks on a value that itself contains an = character, while partition() splits only on the first occurrence.
  • Forgetting the password checker’s has_special check needs not c.isalnum(), not a hardcoded set of symbols, so it correctly accepts any real special character.
  • Letting one malformed log line crash the whole analyzer instead of skipping or flagging lines that don’t match the expected pattern (re.search(...) returning None).

10.19 Hands-on Exercises

Inventory Manager

A small class wrapping a list to manage a collection of items with add/remove/list operations.

class InventoryManager:
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

    def remove(self, item):
        if item in self.items:
            self.items.remove(item)

    def list_all(self):
        return self.items

>>> inv = InventoryManager()
>>> inv.add("server01"); inv.add("server02")
>>> inv.remove("server01")
>>> inv.list_all()
['server02']

Todo App

A list of dicts, each representing one task — a common lightweight data model before reaching for a database.

todos = []

def add_todo(task):
    todos.append({"task": task, "done": False})

def complete_todo(index):
    todos[index]["done"] = True

>>> add_todo("Deploy app"); add_todo("Review PR")
>>> complete_todo(0)
>>> todos
[{'task': 'Deploy app', 'done': True}, {'task': 'Review PR', 'done': False}]

Log Analyzer

Counting occurrences of different severity levels across a list of log lines.

def analyze_logs(lines):
    return {
        "errors": sum(1 for l in lines if "ERROR" in l),
        "warnings": sum(1 for l in lines if "WARNING" in l),
    }

>>> analyze_logs(["INFO x", "ERROR y", "WARNING z", "ERROR w"])
{'errors': 2, 'warnings': 1}

CSV Processor

Parsing a CSV blob into a list of dicts, ready for further filtering or aggregation.

import csv, io

def process_csv(text):
    return list(csv.DictReader(io.StringIO(text)))

>>> process_csv("name,age\nAlice,30\nBob,25")
[{'name': 'Alice', 'age': '30'}, {'name': 'Bob', 'age': '25'}]

Mini Projects

Server inventory tool — group a list of Amazon EC2-style instance records by their current state, the core logic behind any fleet-status dashboard:

def group_by_state(instances):
    result = {}
    for inst in instances:
        result.setdefault(inst["state"], []).append(inst["id"])
    return result

>>> group_by_state([
...     {"id": "i-1", "state": "running"},
...     {"id": "i-2", "state": "stopped"},
...     {"id": "i-3", "state": "running"},
... ])
{'running': ['i-1', 'i-3'], 'stopped': ['i-2']}

Docker tracker — combine the container-membership check from 10.15 Lists in DevOps with a list comprehension to report which expected containers are missing from the running list:

expected = ["nginx", "redis", "postgres", "celery"]
running = ["nginx", "redis", "postgres"]
>>> missing = [c for c in expected if c not in running]
>>> missing
['celery']

Backup manager — keep only the N most recent backups, discarding the rest, a common retention-policy script:

def rotate_backups(backups, keep=3):
    return sorted(backups, reverse=True)[:keep]

>>> rotate_backups([
...     "backup_2026-01-01", "backup_2026-01-05",
...     "backup_2026-01-10", "backup_2026-01-15",
... ], keep=2)
['backup_2026-01-15', 'backup_2026-01-10']
  • AWS inventory report — combine the Amazon EC2 filtering pattern from 10.15 Lists in DevOps with the grouping function above to build a full multi-region, multi-state resource report.
  • Kubernetes inventory — combine the pod-filtering pattern from 10.15 Lists in DevOps with count-by-prefix logic to report how many pods each deployment currently has running.

Quick Interview Answer

“These exercises combine the chapter’s tools into small, realistic programs: the inventory manager and todo app wrap a list inside a class or module-level state with add/remove/query operations; the log analyzer and CSV processor lean on comprehensions and the csv module rather than manual parsing; and the mini projects — grouping Amazon EC2 instances by state, diffing expected vs. running Docker containers, rotating backups by keeping the N most recent — are all variations on the same filtering-and-grouping comprehension pattern applied to real infrastructure data.”

Common Mistakes

  • Mutating self.items from outside the InventoryManager class directly instead of going through add()/remove(), bypassing whatever validation those methods might do.
  • Using a list index directly as a todo item’s permanent identifier (complete_todo(index)) — removing an earlier item shifts every later index, silently completing the wrong task.
  • Sorting backup filenames as plain strings and assuming that sorts chronologically — it only works if the naming format is zero-padded and lexicographically ordered the same as chronological order, as in the ISO-style dates used above.

11.16 Hands-on Exercises

Tuple Operations

Practice combining concatenation, membership, and slicing on a single tuple.

>>> t = (10, 20, 30)
>>> t = t + (40,)     # "append" by concatenating a new tuple
>>> t
(10, 20, 30, 40)
>>> 20 in t
True

Packing & Unpacking

A swap function is the classic demonstration of packing and unpacking working together.

def swap(a, b):
    return b, a

>>> swap(1, 2)
(2, 1)

Data Processing

Returning multiple related results from one function call, using a tuple as the return type.

def minmax(t):
    return min(t), max(t)

>>> minmax((5, 2, 8, 1))
(1, 8)

Mini Projects

Server configuration — a fixed server record, unpacked wherever it’s used; the tuple guarantees the config can’t be accidentally mutated mid-script:

SERVER = ("web01", "10.0.1.5", 8080, "running")

def describe(server):
    name, ip, port, status = server
    return f"{name} ({ip}:{port}) is {status}"

>>> describe(SERVER)
'web01 (10.0.1.5:8080) is running'

AWS region mapper — mapping a fixed tuple of regions to their index, useful for consistent ordering or round-robin selection logic:

def region_mapper(regions):
    return {r: i for i, r in enumerate(regions)}

>>> region_mapper(("us-east-1", "us-west-2", "eu-west-1"))
{'us-east-1': 0, 'us-west-2': 1, 'eu-west-1': 2}

Immutable configuration store — a tiny config object built entirely on tuples internally, guaranteeing nothing can silently rewrite a setting after construction:

class ImmutableConfig:
    def __init__(self, **kwargs):
        self._data = tuple(kwargs.items())

    def get(self, key):
        for k, v in self._data:
            if k == key:
                return v
        return None

>>> config = ImmutableConfig(host="localhost", port=8080)
>>> config.get("host"), config.get("port")
('localhost', 8080)

Quick Interview Answer

“These exercises combine the chapter’s core ideas into small, realistic code: tuple concatenation as the ‘append’ equivalent, the packing/unpacking swap idiom, and multi-value returns via an implicit tuple. The mini projects apply the same pattern to real infrastructure use cases — a server record unpacked into named fields for a description string, mapping a fixed region tuple to indices for round-robin logic, and an ImmutableConfig class that stores its settings as a tuple of (key, value) pairs internally specifically so nothing downstream can silently rewrite a setting after construction.”

Common Mistakes

  • Using t = t + (x,) repeatedly in a hot loop to “grow” a tuple — each concatenation allocates and copies the whole thing; if the collection genuinely needs to grow, use a list and convert to a tuple once at the end.
  • Forgetting the trailing comma when building a single-value tuple inside region_mapper-style code, silently producing the wrong type (see 11.2 Creating Tuples).
  • Implementing ImmutableConfig.get() with a linear scan over many settings when a dict would be both simpler and faster — a tuple of pairs communicates immutability, but doesn’t have to be the only internal representation if lookup performance matters.

12.20 Hands-on Exercises

Duplicate Remover

def remove_duplicates(items):
    return list(set(items))

>>> sorted(remove_duplicates([1, 1, 2, 3, 3]))
[1, 2, 3]

Unique Visitor Counter

Counting distinct visitors from a raw log of (possibly repeated) visitor IDs.

def unique_visitors(visitor_log):
    return len(set(visitor_log))

>>> unique_visitors(["u1", "u2", "u1", "u3"])
3

Package Comparator

Reporting missing and extra packages between an installed set and a required set — the same pattern as 12.16 Sets in DevOps.

def compare_packages(installed, required):
    installed_set, required_set = set(installed), set(required)
    return {
        "missing": required_set - installed_set,
        "extra": installed_set - required_set,
    }

>>> compare_packages(["nginx", "redis"], ["nginx", "postgres"])
{'missing': {'postgres'}, 'extra': {'redis'}}

Quick Interview Answer

“These three exercises build up from the simplest set use case to a realistic infrastructure pattern: remove_duplicates is list(set(...)) in its most basic form, unique_visitors shows that counting distinct items is just len() applied to a set instead of the raw log, and compare_packages combines two differences — required - installed and installed - required — into a single dict report, the exact shape a package-audit or inventory script needs in practice.”

Common Mistakes

  • Returning set(items) from remove_duplicates when a list was the expected return type — wrap with list(...) to match the original type.
  • Computing unique_visitors by manually looping and tracking a seen list with in checks, instead of the direct len(set(...)) one-liner.
  • Building compare_packages with only one direction of difference, silently missing either the “missing” or “extra” half of the comparison.