Skip to main content
APPLIED PYTHON / IT AUTOMATION / LESSON 03

Filesystem & Structured Data Pipelines.

Turn a configurable Python command into a trustworthy local data workflow. Find files safely, decode text, validate real CSV and JSON, protect existing reports, and make failure visible.

Intermediate90–115 minutes plus an optional local projectInteractive pipeline explorer8 Python challengesLocal project exercise
00

What you'll learn

Lesson 01 taught you to build a local project and Lesson 02 taught you to make it configurable from a command line. This lesson connects those skills to real filesystem operations and structured data. Our data is fictional, but the Python parsing and validation are real.

  • Structure a file pipeline with explicit location, reading, parsing, validation, transformation and publication stages.
  • Use pathlib to reason about relative paths, root boundaries and local file access without relying on text prefixes.
  • Specify UTF-8 encodings, recognize BOM-bearing CSV exports and open CSV files with newline=''.
  • Read and validate CSV headers and rows with DictReader, including quotes, extra fields and missing values.
  • Normalize only known status values and report malformed, duplicate or unknown records separately.
  • Parse and serialize stable JSON while validating schema, Unicode behavior and nonstandard numeric values.
  • Bound file inputs and distinguish missing, unreadable and oversized files from valid empty exports.
  • Stage output in the target folder and replace existing reports only after validation and an explicit write decision.
  • Combine a CLI from Lesson 02 with an offline file pipeline that can be tested safely in both browser and local environments.
Where code executes: The visual explorer is a JavaScript model for learning and never writes files. The Python workbench performs real Python operations only inside an isolated browser filesystem. The optional local project script runs on your own computer and can write a report only when you explicitly provide --write.

Prerequisite: Complete Command-Line Tools & Configuration or know how to use argparse and run a local Python script. If CSV, JSON, exceptions or functions still feel unfamiliar, review the Python Fundamentals path before continuing.

FROM RAW FILE TO TRUSTWORTHY REPORT
01

Think in pipeline stages, not one giant script

A working file pipeline is more than calling open() and writing a result. A reliable tool has a clear sequence: locate the requested file, read it with an intentional encoding, parse its format, validate what the data means, transform accepted rows, then publish a report. Each stage should make its own result explicit so an invalid source cannot turn into a successful-looking empty report. Use fictional inventory exports first; never aim beginner scripts at production shares or customer data.

Inputs need a contract

Define expected filenames, encoding, required columns, accepted status values and maximum file size. A file that exists but has no expected header is not equivalent to a valid export with zero devices.

Separate functions by responsibility

Keep read_source(path), parse_inventory(text), build_report(rows), and write_report(output, data) separate. Test each with changed inputs so you can identify which stage failed.

Leave the source untouched

Read the export without overwriting it. Build a new report in a separate output location; dry-run or preview modes should reveal the proposed result without creating files.

PYTHON / PREDICT BEFORE RUNNINGRead the example
def summarize(rows):
    offline = sum(1 for row in rows if row['status'] == 'offline')
    return {'total': len(rows), 'offline': offline}
print(summarize([{'status':'offline'},{'status':'online'}]))
Why is a CSV file missing required columns different from a valid file containing no device rows? Reveal the explanation after predicting.

Missing columns mean the input doesn't meet the schema and the tool cannot interpret it reliably. An empty but valid CSV export can legitimately produce zero devices. Treating both as zero would hide a broken import.

INTERACTIVE CSV → JSON PIPELINE · SIMULATED

What happens when the input is imperfect?

Change a fictional CSV export and follow it through six stages. This explorer models the expected decisions; the coding clinic below executes real Python file and CSV operations in an isolated browser filesystem.

SELECTED STAGE / READ

Inspect the source text without changing it

SchemaAccepted
Accepted2
Rejected rows3
Duplicate hosts1
DATA / NO FILES WRITTEN
host,status,site
SW-01,online,HQ
AP-02,offline,BR1
AP-02,online,BR1
FW-01,offline,"HQ, East"
BAD,maintenance,HQ

Choose the next stage to see how rows turn into a deterministic summary. All data stays inside this page. No live systems are contacted.

Remember: Give each pipeline stage one responsibility and distinguish invalid evidence from an acceptable empty dataset.
PATHS ARE DATA AND PERMISSIONS MATTER
02

Use pathlib to locate and constrain files

pathlib.Path represents filesystem paths and works across operating systems. A relative path such as samples/inventory.csv is usually resolved against the process working directory, which can differ when launched from an editor, Task Scheduler or a service. Use a documented project root or explicit absolute input path rather than assuming the current directory. If a tool accepts a user-supplied filename under a known data folder, verify the resolved path stays inside that folder. Be aware that resolving paths and then opening them isn't a complete defense against a malicious actor changing symlinks concurrently; for this learning tool we use only trusted local files.

Relative and absolute paths

Path('samples/inventory.csv') refers to the current working directory; Path(__file__).resolve().parent refers to the location of the running script. Don't confuse them.

Check the resolved boundary

Let base = Path('samples').resolve(). Verify the target resolves under base, not merely that its text starts with the same letters: /reports and /reports-old are different directories.

Use the right operation

exists() tells whether an entry exists but doesn't establish that it is an ordinary readable file. Use is_file(), handle OSError when opening, and avoid deleting or rewriting unexpected paths.

PYTHON / PREDICT BEFORE RUNNINGRead the example
from pathlib import Path
base = Path('samples').resolve()
candidate = (base / 'inventory.csv').resolve()
allowed = candidate.is_relative_to(base)
print(allowed)
Why is `str(candidate).startswith(str(base))` an unsafe substitute for a proper path boundary check? Reveal the explanation after predicting.

String prefixes aren't path components. /reports-old may start with /reports even though it is outside the intended directory. Path.is_relative_to compares actual path components after resolution.

SYNTAX BRIDGE / BEFORE YOU CODE

Identify path components before trusting a location

The safe-file challenge asks for checks that don't appear together in the main example. Use a resolved base, examine relative paths structurally, and reject `..` before joining and resolving.

Python / no files createdRUNNABLE PYTHON · READ FIRST
from pathlib import Path

base = Path('/northstar/incoming').resolve()
name = Path('exports/inventory.csv')
allowed = (not name.is_absolute() and '..' not in name.parts)
target = (base / name).resolve()
print(allowed and target != base and target.is_relative_to(base))

Path.parts checks components; Path.is_absolute rejects an absolute filename; .resolve and .is_relative_to check the resolved boundary. These are useful checks for a trusted practice directory but not a substitute for race-resistant opening of untrusted symlinks.

Predict and explain: Should a filename `../secrets.csv` be joined to the approved root and trusted because it starts as a relative path?

No. Reject the parent traversal component and verify the final resolved path is within the approved base.

Remember: Resolve trusted paths against a defined root and validate the intended boundary; path text alone is not a security check.
READ THE SAME BYTES CORRECTLY
03

Specify encodings, BOM behavior and newlines

File contents start as bytes. When Python reads text, it decodes those bytes with an encoding. Specify encoding='utf-8' rather than relying on the machine's default, and use 'utf-8-sig' when a CSV exporter may include a UTF-8 byte-order mark: that codec removes a leading BOM while also reading normal UTF-8. A wrong encoding may raise UnicodeDecodeError or produce damaged text. For CSV, open(path, newline='', encoding='utf-8-sig') so Python's csv module can handle record newlines correctly, including quoted fields containing line breaks. Do not solve encoding errors by blindly ignoring undecodable bytes.

Text is not raw bytes

Path.read_text(encoding='utf-8') decodes text and returns str. Path.read_bytes() returns bytes. An input failure should be surfaced rather than reported as an empty file.

UTF-8 with a BOM

Some spreadsheets export UTF-8 data starting with a BOM. Using utf-8-sig avoids an unexpected '\ufeffhost' column name in a CSV header.

Line endings are format-specific

Text files can use LF or CRLF. The csv module recommends newline='' when opening files because quoted fields can contain line breaks. Don't manually split CSV into lines and commas.

PYTHON / PREDICT BEFORE RUNNINGRead the example
from io import StringIO
import csv
sample = '\ufeffhost,status\nSW-01,online\n'
with StringIO(sample.lstrip('\ufeff'), newline='') as stream:
    reader = csv.DictReader(stream)
    print(reader.fieldnames)
What can happen if a UTF-8 BOM remains at the beginning of a CSV header named host? Reveal the explanation after predicting.

The actual first column may be named '\ufeffhost' instead of 'host', so a strict required-column validation will fail even though the header looks normal in an editor. Decode with utf-8-sig when reading possible BOM-bearing files.

SYNTAX BRIDGE / BEFORE YOU CODE

Read local text with the exact codec and newline policy

The browser challenge asks you to read an actual temporary file, not just manipulate a StringIO string. Open it with an explicit mode, encoding and newline option.

Python / using a disposable temporary directoryRUNNABLE PYTHON · READ FIRST
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as folder:
    path = Path(folder) / 'inventory.csv'
    path.write_bytes(b'\xef\xbb\xbfhost,status\nSW-01,online\n')
    with open(path, 'r', encoding='utf-8-sig', newline='') as stream:
        text = stream.read()
    print(text.splitlines()[0])

The temporary directory is removed automatically when the block exits. utf-8-sig recognizes the optional BOM, and newline='' lets csv handle record boundaries. Errors such as FileNotFoundError and UnicodeDecodeError are not valid empty inputs.

Predict and explain: Why shouldn't you return an empty string when open() raises FileNotFoundError?

An absent input is a failed collection, not a successfully read empty file. The calling pipeline must be able to tell the difference.

Remember: Choose and document the encoding. For CSV, use newline='' and handle BOMs without silently discarding invalid bytes.
A CSV FILE IS NOT JUST SPLIT(',')
04

Parse CSV with DictReader and inspect row structure

Real CSV can include commas, quotes and even newlines inside quoted fields. csv.DictReader maps each row to column names, which makes the logic independent of a particular column order. It still needs schema validation: fieldnames can be missing or duplicated, and extra columns appear under a None key by default while short rows get None values. Treat suspicious row shapes as invalid evidence, not values to guess. When exporting with DictWriter, declare the output field order and use writeheader(); a stable schema helps downstream tools and spreadsheets.

Quoted commas aren't extra fields

A row such as 'SW-01,"HQ, East",offline' has three fields. split(',') incorrectly produces four; csv.DictReader preserves the quoted site field.

Header validation matters

Check fieldnames before processing rows. Reject missing or repeated required headers, including accidental duplicates; rearranged columns can be valid when names match.

Malformed rows should be counted

Extra fields, missing fields, blank hostnames and unsupported statuses should be recorded as rejected rows. Keep accepted and rejected counts visible so a small successful report doesn't hide bad input.

PYTHON / PREDICT BEFORE RUNNINGRead the example
import csv
from io import StringIO
csv_text = 'status,host,site\noffline,SW-01,"HQ, East"\n'
rows = list(csv.DictReader(StringIO(csv_text, newline='')))
print(rows[0]['site'])
Why does the site print as 'HQ, East' instead of becoming two separate columns? Reveal the explanation after predicting.

The csv parser honors double quotes around the field and treats its comma as content rather than a column delimiter. A simple text.split(',') would not respect that structure.

Remember: Use the csv module for delimited data, validate fieldnames and count malformed rows instead of hiding them.
BAD DATA DOESN'T EQUAL HEALTHY DEVICES
05

Normalize and validate before counting anything

Parsing turns bytes into fields, but that isn't the same as proving those fields are usable. Trim whitespace around host and status, normalize known status values, and define what to do with blank, duplicated or unexpected records. The example treats online and offline as known statuses; maintenance, missing and misspelled statuses are not automatically online. For deterministic reporting, sort accepted hosts explicitly. If duplicates are ambiguous, reject or flag them instead of silently preferring the first record. Return structured validation details so a technician can tell clean exports from partially rejected data.

Normalization versus invention

' OFFLINE ' may safely normalize to 'offline'. But 'maintenence' should not be silently corrected to 'online': the export isn't supplying a known status.

Duplicates are operational evidence

If two different statuses claim the same hostname, picking one would invent a health conclusion. Mark the duplicate for review or fail according to a documented policy.

Show accepted and rejected

A report with two accepted rows and three rejected should say so. Prefer named keys such as accepted, rejected and errors over a bare success Boolean.

PYTHON / PREDICT BEFORE RUNNINGRead the example
allowed={'online','offline'}
raw = {'host':' AP-02 ', 'status':' OFFLINE '}
host=raw['host'].strip()
status=raw['status'].strip().lower()
print(host, status, status in allowed)
Should an unknown status be counted as online because it isn't the literal string 'offline'? Reveal the explanation after predicting.

No. Unknown is not a synonym for healthy. Validate against explicitly allowed statuses and report unrecognized values separately so you don't understate operational risk.

SYNTAX BRIDGE / BEFORE YOU CODE

Count duplicates before deciding which records to accept

The local example uses collections.Counter, which counts occurrences. Without this introduction, the learner sees a new library call in a lengthy script and has to infer its purpose.

Python / Counter is a frequency dictionaryRUNNABLE PYTHON · READ FIRST
from collections import Counter

hosts = ['SW-01', 'AP-02', 'sw-01']
normalized = [host.upper() for host in hosts]
frequency = Counter(normalized)
print(frequency['SW-01'])
print(sorted(host for host, count in frequency.items() if count == 1))

Counter is a standard-library dictionary-like class. It reports the number of occurrences of each key. Count normalized hostnames first, then exclude all conflicting occurrences rather than accepting an arbitrary one.

Predict and explain: Why is accepting the first SW-01 row and ignoring a later conflicting sw-01 row unsafe?

It invents a trusted version of a conflict. Record or reject the duplicate and avoid claiming either row is authoritative.

Remember: Validate semantics before transformation. Keep rejected rows visible and never turn unknown status into an assumed success.
STRUCTURED OUTPUT WITH A DEFINED SHAPE
06

Decode and emit predictable JSON

JSON stores objects, arrays, strings, numbers, booleans and null. json.loads() can parse valid JSON text, but a valid JSON document may still have the wrong top-level shape; check that the expected object and collection keys are present. Use json.dumps(report, sort_keys=True, indent=2, ensure_ascii=False) for readable, deterministic output when field ordering and Unicode are important to your project. `ensure_ascii=False` retains Unicode characters in the text, so write the result with UTF-8. Treat NaN and Infinity as invalid reporting numbers rather than relying on Python's permissive default output; allow_nan=False prevents those nonstandard values from being emitted.

Syntax first, schema second

json.loads('[]') succeeds, but a pipeline expecting an object with a devices array should reject that shape. Missing required keys are not valid zero values.

Stable output

Sort keys, normalize rows, and choose deterministic ordering for lists. This makes different runs easier to compare and keeps diff noise out of troubleshooting reports.

Unicode needs deliberate handling

json.dumps(...,ensure_ascii=False) retains characters such as São Paulo in output; file writes should specify encoding='utf-8' to match.

PYTHON / PREDICT BEFORE RUNNINGRead the example
import json
report={'site':'São Paulo','offline':1,'total':2}
print(json.dumps(report,sort_keys=True,ensure_ascii=False))
Why doesn't a successful json.loads() call prove that data is safe to pass into a report builder? Reveal the explanation after predicting.

It confirms only JSON syntax. A list, integer or string is valid JSON but may violate the expected object schema. Validate type and required fields before deriving any counts.

Remember: Separate JSON syntax from schema validity and generate stable UTF-8 reports with explicit serialization settings.
LIMIT WORK BEFORE ALLOCATING MEMORY
07

Bound file size, row counts and resource use

A file that looks like a harmless export can be unexpectedly large or malformed. Before reading an entire file, apply a sensible byte limit for your tool and require a known source. Avoid calling read_text() on unknown multi-gigabyte exports, and remember size checks are not race-proof on files that other processes can alter. A first small utility can read bounded inputs entirely; later large-scale pipelines can iterate rows instead of building a huge list. Design errors so a too-large or unreadable export is rejected, not reported as a clean inventory with zero devices.

Whole-file versus streaming

Path.read_text() is simple for small verified inputs, but loads the whole decoded file. A streaming loop or csv.DictReader over a file handle consumes records incrementally.

Limits should be intentional

Document a maximum input size and a maximum number of accepted rows, then reject oversized inputs. Python's file size may change between inspection and opening, so additional checks may be required in adversarial environments.

Catch expected failures

FileNotFoundError, UnicodeDecodeError and PermissionError tell different stories. Don't swallow all exceptions; return a clear error category or let the caller handle it.

PYTHON / PREDICT BEFORE RUNNINGRead the example
from pathlib import Path
path=Path('samples/inventory.csv')
max_bytes=1024*1024
if path.is_file() and path.stat().st_size<=max_bytes:
    print('Size is acceptable to inspect')
else:
    print('Missing or outside size limit')
Can a size check alone guarantee that a file stays below the limit while another process is changing it? Reveal the explanation after predicting.

No. Between stat() and read(), another process may change the content. Size limits are useful for expected local data, but strict bounds need to be enforced during reading too in untrusted environments.

Remember: Choose explicit limits and communicate when input cannot be trusted or processed; never translate failures into zero healthy records.
BUILD A REPORT BEFORE YOU REPLACE ONE
08

Stage output, verify it, then replace deliberately

Writing directly over last week's valid report can leave a half-written file if your process stops. A safer local pattern is to build the complete report in memory, write a temporary file in the destination directory, close it, then use os.replace(temp, destination) to replace the old report. On supported local filesystems, replacement is atomic at the filename level; it is not a guarantee of crash durability or universal network-share behavior. os.replace overwrites the destination, so require an approved output location and explicit overwrite policy. Clean up temporary artifacts on failure and keep your original input export unchanged.

Temporary file in the same directory

tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=destination.parent, delete=False) places staging near the target so replacement usually stays on the same filesystem.

Validate before publishing

Build and validate the entire JSON report first. If parsing or validation fails, do not touch the existing output. Verify report structure before attempting an overwrite.

The real boundary

A browser exercise writes only in Pyodide's isolated virtual filesystem. A local script can write actual files, so always inspect the chosen directory and protect valuable outputs.

PYTHON / PREDICT BEFORE RUNNINGRead the example
import json
payload={'site':'HQ','total':2}
text=json.dumps(payload,sort_keys=True,ensure_ascii=False,allow_nan=False)
print(text)
Why is writing a staging file preferable to immediately opening an existing report with 'w'? Reveal the explanation after predicting.

Opening the final report with 'w' truncates it immediately. Writing and validating a separate candidate first helps preserve the previous report if processing fails before replacement. Replacement still needs a deliberate overwrite policy.

SYNTAX BRIDGE / BEFORE YOU CODE

Stage, replace and clean up a report step by step

The staged-write challenge requires a temporary file, explicit overwrite policy and cleanup. This example shows the precise Python APIs first; the later exercise makes you add validation and rejection paths independently.

Python / disposable report directoryRUNNABLE PYTHON · READ FIRST
import os
from pathlib import Path
from tempfile import TemporaryDirectory, NamedTemporaryFile

with TemporaryDirectory() as folder:
    destination = Path(folder) / 'report.json'
    temporary = None
    try:
        with NamedTemporaryFile('w', encoding='utf-8',
                                dir=folder, delete=False) as stream:
            temporary = Path(stream.name)
            stream.write('{"total": 2}\n')
        if destination.exists():
            raise FileExistsError('overwrite not approved')
        os.replace(temporary, destination)
        temporary = None
        print(destination.read_text().strip())
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

Create a candidate in the same directory, close it before replacing the destination, and remove an abandoned candidate in finally. os.replace overwrites an existing target, so real code must require deliberate overwrite approval. This example stays in a disposable directory.

Predict and explain: If JSON serialization fails, should the existing report be opened with mode 'w' before reporting the error?

No. Build and validate the complete candidate first. Opening the existing report with 'w' would truncate it immediately.

Remember: Validate the entire report before staging it. Replace deliberately, and don't confuse atomic visibility with crash-proof persistence.
CONNECT LESSONS 01, 02 AND 03
09

Turn a CLI into a repeatable inventory file workflow

The Applied Python project can now accept --input and --output arguments, find a known local inventory CSV, validate required host and status fields, and produce a deterministic JSON report. Keep parsing and transformations as pure functions where possible; let main(argv) coordinate file operations and return an exit code. Use --dry-run or a preview default to inspect the proposed report without writing. A useful runbook says which files may be touched, what a successful report contains, and what the tool does when an input is absent or malformed. Test missing headers, quoted commas, duplicate hosts, empty inventories, and an existing output report before calling the workflow reliable.

Your CLI remains the entry point

The argparse contract from Lesson 02 supplies input and output paths. The pipeline functions do the parsing and validation without needing to know about command-line options.

Report status must be observable

A valid empty export may produce a report with total 0. Missing columns or unreadable input must produce an error and a nonzero exit code without replacing a previously valid report.

Read-only preview is the default learning mode

Use fictional local files and inspect the JSON result in memory before adding an explicit --write switch. Never aim a tutorial script at a live share or sensitive export.

PYTHON / PREDICT BEFORE RUNNINGRead the example
def report_from_rows(rows):
    accepted = [r for r in rows if r['status']=='offline']
    return {'total':len(rows),'offline_hosts':sorted(r['host'] for r in accepted)}
print(report_from_rows([{'host':'AP-02','status':'offline'}]))
What should happen when a report already exists but the new CSV source has no required host column? Reveal the explanation after predicting.

The pipeline should reject the invalid schema, report the reason, return a nonzero status and preserve the existing output. A malformed source is not a valid empty inventory and must not overwrite reliable evidence.

Remember: Connect CLI inputs to validated, bounded file processing, then publish only after successful checks; protect old reports when new input is invalid.
LOCAL PYTHON / PRACTICE THE COMPLETE WORKFLOW
10

Run a guarded CSV-to-JSON project locally

Build on the local northstar-python-tool project from Lesson 01. This example uses only the standard library and deliberately reads a fictional CSV in samples/. It previews a JSON report by default, leaving files untouched. An explicit --write saves a report inside reports/; a separate --replace is required if that output already exists.

Prepare a fictional sample

Inside the project root, manually create a folder samples and save inventory.csv there using UTF-8 encoding. The data below intentionally includes a duplicate hostname, an unknown status and a quoted comma. Create a separate empty reports folder for the output.

samples/inventory.csvFictional records
host,status,site
SW-01,online,HQ
AP-02,offline,BR1
AP-02,online,BR1
FW-01,offline,"HQ, East"
BAD,maintenance,HQ

Know the safe boundaries

The local script reads only from the project's samples folder and writes only inside reports. It rejects absolute filenames and traversal segments, limits input to 256 KiB, handles BOM-bearing UTF-8 exports, and rejects missing CSV columns before publication. These checks are suitable for a trusted learning workspace, not a complete defense against adversarial symlink races.

Save the following complete Python source as inventory_pipeline.py in your project root. Read the main() function and report logic before copying it; the code illustrates the intended design rather than replacing the independent challenges in Practice.

inventory_pipeline.py / RUN ON YOUR COMPUTERStandard library only
import argparse
import csv
import json
import os
import sys
from collections import Counter
from io import StringIO
from pathlib import Path
from tempfile import NamedTemporaryFile

ROOT = Path(__file__).resolve().parent
MAX_BYTES = 256 * 1024

def within(folder, filename):
    name = Path(filename)
    base = (ROOT / folder).resolve()
    if not filename.strip() or name.is_absolute() or ".." in name.parts:
        raise ValueError("Provide a relative filename inside " + folder)
    target = (base / name).resolve()
    if target == base or not target.is_relative_to(base):
        raise ValueError("Path leaves the allowed " + folder + " directory")
    return target

def read_csv(path):
    with path.open("rb") as stream:
        raw = stream.read(MAX_BYTES + 1)
    if len(raw) > MAX_BYTES:
        raise ValueError("Input exceeds 256 KiB")
    return raw.decode("utf-8-sig")

def analyze(text):
    reader = csv.DictReader(StringIO(text, newline=""), strict=True)
    fields = reader.fieldnames or []
    if len(fields) != len(set(fields)) or not {"host","status"} <= set(fields):
        raise ValueError("Missing or repeated host/status columns")
    possible = []
    rejected = 0
    for row in reader:
        host, status = row.get("host"), row.get("status")
        if (None in row or any(value is None for value in row.values())
            or not isinstance(host, str) or not host.strip()
            or not isinstance(status, str)
            or status.strip().lower() not in {"online","offline"}):
            rejected += 1
            continue
        possible.append((host.strip().upper(), status.strip().lower()))
    counts = Counter(host for host, status in possible)
    accepted = [(host, status) for host, status in possible if counts[host] == 1]
    rejected += len(possible) - len(accepted)
    return {
        "accepted": len(accepted),
        "rejected": rejected,
        "offline_hosts": sorted(host for host, status in accepted if status == "offline")
    }

def write_staged(path, report, replace=False):
    if not path.parent.is_dir():
        raise ValueError("Output directory does not exist")
    if path.exists() and not replace:
        raise ValueError("Output exists; use --replace to approve overwrite")
    text = json.dumps(report, sort_keys=True, ensure_ascii=False, allow_nan=False, indent=2) + "\n"
    temporary = None
    try:
        with NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
                                prefix=".inventory-", suffix=".tmp", delete=False) as stream:
            temporary = Path(stream.name)
            stream.write(text)
        os.replace(temporary, path)
        temporary = None
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

def main(argv=None):
    parser = argparse.ArgumentParser(description="Fictional, offline inventory file pipeline")
    parser.add_argument("--input", default="inventory.csv", help="Filename within samples/")
    parser.add_argument("--output", default="summary.json", help="Filename within reports/")
    parser.add_argument("--write", action="store_true", help="Publish report to local reports/")
    parser.add_argument("--replace", action="store_true", help="Allow replacing an existing output")
    args = parser.parse_args(argv)
    if args.replace and not args.write:
        parser.error("--replace requires --write")
    try:
        source = within("samples", args.input)
        destination = within("reports", args.output)
        report = analyze(read_csv(source))
        if args.write:
            write_staged(destination, report, replace=args.replace)
            print("Saved report to " + str(destination))
        else:
            print(json.dumps(report, sort_keys=True, ensure_ascii=False, indent=2))
        return 0
    except (ValueError, OSError, UnicodeError, csv.Error) as exc:
        print("Pipeline error: " + str(exc), file=sys.stderr)
        return 1

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

Start with a read-only preview

python inventory_pipeline.pypython inventory_pipeline.py --helppython inventory_pipeline.py --writepython inventory_pipeline.py --write --replace

Run these inside the activated virtual environment, or invoke its Python executable directly. Review the preview first and run the write command only against your own disposable practice folder.

Verify the result and failure behavior

The sample contains two accepted devices, three rejected records (both AP-02 rows plus the unsupported status), and one offline accepted host: FW-01. Before using --replace, deliberately break the CSV header and confirm the script reports an error and leaves any existing report unchanged. This is the behavior we want in real automation.

11 / PRACTICE REAL PYTHON
11

Write the pipeline logic yourself

The first exercise checks pathname boundaries. The full coding clinic then explores actual UTF-8 file reads, malformed CSV rows, duplicate hosts, JSON schema checks, deterministic reports and staged writes. The last task makes you design an end-to-end inventory analyzer tested against unfamiliar data.

JOELEARNS / PYTHON WORKBENCH

Write it. Run it. Understand it.

Work with pathlib, real temporary Python files, CSV/JSON schema validation and staged reports inside the isolated browser filesystem.

Python 3.14 · in-browser
CHALLENGE 01 / 8 · INTERMEDIATE

Resolve a file inside an approved folder

Write safe_file(root, filename) using pathlib.Path. Return a resolved Path only when filename is a relative file path inside root, has no '..' path component, isn't the root directory itself, and resolves inside the root. Otherwise return None. For root '/northstar/incoming' and 'inventory.csv', print the complete resolved path. The grader changes folder names and tries traversal, absolute and sibling-prefix paths. Don't create or delete any files.

EXPECTED OUTPUT/northstar/incoming/inventory.csv
script.py editableCtrl / Cmd + Enter to run
Your code is stored only in session storage when enabled. Never paste secrets. Clearing this option clears saved drafts, not the editor.
Code executes only when you choose Run or Check. No account needed.
HINTS · OPTIONAL

Try solving it before asking for help. Hints never show a completed program.

PYTHON OUTPUTReady

Your printed output and errors will appear here.

VARIABLE TRACER

Run the code, then step through the executed lines to watch variable values change.

About this environment: The Python runtime is loaded on demand from a version-pinned CDN and runs inside a sandboxed, opaque-origin browser frame. Runs have a time limit. OS/network automation is outside this introductory workbench. If your code calls input(), a response panel appears to supply text to those calls. Don't paste secrets or untrusted scripts.

Next required step: complete the 8-challenge Practice milestone →
NEXT / APPLY THE CONCEPTS

Know why the output is trustworthy.

A working pipeline should explain which rows were accepted, which were rejected and why publishing is safe. Try the exercises with changed input before treating a passing sample as proof of reliability.

Continue to all eight file-pipeline challenges →Next: REST APIs & Authentication →Applied Python path →
12

Official references

The official Python documentation is the source of truth for path methods, CSV parsing, JSON serialization and temporary-file behavior. Some filesystem guarantees vary across platforms; read the caveats before adapting local scripts for operational use.

NEXT REQUIRED STEP · APPLIED PYTHON FOR IT AUTOMATION

Complete the Practice milestone

You've reached the end of this written lesson. Your next step is practice—complete Filesystem & Structured Data Pipelines Coding Clinic before proceeding to a new lesson. This is milestone 6 of 13. You can revisit the explanation whenever you need it.