← Course Home
Module 10

Files & Env Vars

Reading and writing files, numbering and sorting output, keeping secrets out of code — and one final script that pulls together everything from this whole course.

Concept

Why Files Matter for Scripts

So far every script has printed to the screen and forgotten everything when it ends. Real IT/SecOps scripts need to save reports, read config, and log results — all of that means reading and writing files.

Lab 1

Name one reason a script might need to WRITE a file (not just print).

e.g. saving a report for later, or for someone else to read
Lab 2

Name one reason a script might need to READ a file.

e.g. loading config, or a list of usernames to process
Lab 3

What happens to everything a script print()s once the script ends? (one word)

gone
Lab 4

What happens to what a script WRITES to a file once the script ends?

it stays on disk
Lab 5

Name one IT/SecOps task from this course's fixture that would make more sense saved to a file than printed to a screen.

e.g. the department report, for someone else to open later
Concept

open() and the File Handle

open() gives you a "file handle" — an object connected to a file on disk. You read or write through it, then must close() it when done, or changes might not actually get saved.

f = open("report.txt", "w")
f.write("hello\n")
f.close()
Lab 1

Run the example above. Confirm a file called report.txt now exists with "hello" in it.

file created, contains "hello"
Lab 2

Print type(f) right after open() — what kind of object is it?

a file/TextIOWrapper object
Lab 3

Write to a file WITHOUT calling .close() afterward, in a separate short script. What's risky about this?

the write might not actually be saved to disk
Lab 4

Open a file that doesn't exist yet with "w" mode. Does it crash, or create the file?

creates it
Lab 5

Try to open a file that doesn't exist with "r" mode (read). What happens?

CrashesFileNotFoundError
Now go practice

Open problems/02_open_and_handle.py and do all 5 problems locally.

Concept

Context Managers: with

with opens a file and closes it automatically for you, even if an error happens in between. It's the standard, safer way to work with files — you almost never call open()/close() by hand in real code.

with open("report.txt", "w") as f:
    f.write("hello\n")
# file is automatically closed here, even if something crashed above
Lab 1

Run the example above. Confirm the file is written correctly.

file created, contains "hello"
Lab 2

What word right after open(...) names the file handle inside the with block?

as — here, f
Lab 3

Try accessing f AFTER the with block ends and writing to it. What happens?

CrashesValueError: I/O operation on closed file
Lab 4

In one sentence: why is with safer than calling open()/close() by hand?

the file closes automatically even if an error happens mid-block
Lab 5

Write a with block that writes two lines to a file using two separate .write() calls.

file has both lines
Now go practice

Open problems/03_with_statement.py and do all 5 problems locally.

Concept

Reading a File ("r" mode)

"r" mode opens a file for reading. .read() gets the whole file as one string. .readlines() gets a list of lines. Looping directly over the file object gives you one line at a time.

with open("report.txt", "r") as f:
    content = f.read()
print(content)

with open("report.txt", "r") as f:
    for line in f:
        print(line.strip())
Lab 1

Write a 3-line file, then .read() it. Print the type of what you got back.

<class 'str'>
Lab 2

Read the same file with .readlines(). Print the type, then len() of the result.

<class 'list'> then 3
Lab 3

Loop directly over the file object (no .readlines()), printing each line WITHOUT .strip(). What extra character shows up?

a trailing \n on each line
Lab 4

Now loop again WITH .strip(). What's different?

no trailing newline
Lab 5

Read a file that doesn't exist. Wrap it in try/except FileNotFoundError, printing "file not found".

file not found
Now go practice

Open problems/04_reading_files.py and do all 5 problems locally.

Concept

Checkpoint: Reading and Looping Over Real Lines

Combine everything: write a file with real data, then read it back and process each line.

Lab 1

Write a file with one department name per line: IT, Security, Engineering, Sales.

4-line file created
Lab 2

Read it back with with/"r", printing each stripped line.

4 lines print, no \n
Lab 3

Count how many lines the file has, using your loop from Lab 2.

4
Lab 4

Read the file again, building a Python list of the stripped department names (not just printing).

['IT', 'Security', 'Engineering', 'Sales']
Lab 5 · Checkpoint

Write a function read_department_list(path) that returns the list from Lab 4, guarding with try/except for a missing file (return [] instead of crashing). Test it on both a real path and a fake one.

→ real list, then []
Now go practice

Open problems/05_reading_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 2-4.

Concept

Writing a File ("w" mode)

"w" mode opens a file for writing — and completely erases whatever was there before, every time you open it. .write() writes a string exactly as given; it doesn't add newlines for you.

with open("report.txt", "w") as f:
    f.write("first line\n")
    f.write("second line\n")
Lab 1

Run the example above. Read the file back and print its contents.

first line\nsecond line\n
Lab 2

Write a file with content A, then open it AGAIN with "w" mode and write content B. Read it back — is A still there?

No — "w" erased it, only B remains
Lab 3

Call .write("no newline") twice in a row (no \n). Read the file back — what do you see?

no newlineno newline (both stuck together)
Lab 4

Write a list of 3 department names to a file, one per line, using a loop and .write().

3-line file
Lab 5

Write a function save_lines(path, lines) that takes a list of strings and writes each on its own line. Test it.

Now go practice

Open problems/06_writing_files.py and do all 5 problems locally.

Concept

Appending ("a" mode)

"a" mode adds to the END of a file instead of erasing it — useful for logs, where you want to keep adding without losing history.

ModeIf file existsIf file doesn't exist
"r"reads itcrashes
"w"erases, then writescreates it
"a"adds to the endcreates it
Lab 1

Write "first\n" to a new file with "w". Then open it again with "a" and write "second\n". Read it back.

first\nsecond\n, both kept
Lab 2

Open a brand-new (never-created) filename with "a" mode directly. Does it crash?

No — creates the file
Lab 3

Call "a" mode three times in a row, each time writing one line. How many lines does the file have?

3
Lab 4

In one sentence: why would a logging script use "a" instead of "w"?

so each run adds to the log instead of erasing prior runs
Lab 5

Try opening a file that doesn't exist with "r" mode. Confirm the table above — it crashes.

CrashesFileNotFoundError
Now go practice

Open problems/07_appending_files.py and do all 5 problems locally.

Concept

json.load()

Module 9 taught json.loads() for text you already have. json.load() (no "s") reads directly from an open file handle — the two are almost always confused, so notice the difference in the name.

import json

with open("data.json", "w") as f:
    json.dump({"department": "IT", "count": 2}, f)

with open("data.json", "r") as f:
    data = json.load(f)
print(data)
print(type(data))
Lab 1

Run the example above. Confirm both printed lines.

{'department': 'IT', 'count': 2} then <class 'dict'>
Lab 2

What's the name of the function that writes JSON directly to a file (used above)?

json.dump()
Lab 3

Try json.loads(f) (the Module 9 version, with the "s") on an open file handle instead of json.load(f). What happens?

CrashesTypeError, it expects a string, not a file object
Lab 4

Write a list of 3 dicts to a file with json.dump(), then read it back with json.load(). Print len() of the result.

3
Lab 5

Write a function load_config(path) that reads and returns JSON from a file, returning an empty dict {} if the file doesn't exist.

Now go practice

Open problems/08_json_load.py and do all 5 problems locally.

Concept

sorted() With a Custom Key

sorted() normally sorts by natural order. Pass key= to tell it exactly what to sort BY — like a dict's specific field, or a case-insensitive version of a string.

names = ["Charlie", "alice", "Bob"]
print(sorted(names))
print(sorted(names, key=str.lower))

users = [{"name": "Rivera"}, {"name": "Chen"}, {"name": "Morris"}]
print(sorted(users, key=lambda u: u["name"]))
Lab 1

Run the first two lines above. Why do plain sorted(names) and sorted(names, key=str.lower) give different orders?

plain sort is case-sensitive (uppercase sorts before lowercase); key=str.lower ignores case
Lab 2

Run the users example. Print the sorted list of just names.

['Chen', 'Morris', 'Rivera']
Lab 3

Sort the real USERS fixture by profile["lastName"]. Print the first result's first name.

Amy (Chen sorts first)
Lab 4

Add reverse=True to your sort from Lab 3. What changes?

now sorted Z-to-A instead of A-to-Z
Lab 5

Sort USERS by number of groups (len(u["groups"])) using a lambda key. Print the first result's first name.

someone with the fewest groups (0)
Now go practice

Open problems/09_sorted_key.py and do all 5 problems locally.

Concept

Checkpoint: enumerate() for Numbered Output

enumerate() gives you both the index and the item while looping — perfect for numbered reports, instead of manually tracking a counter variable.

names = ["Rivera", "Chen", "Morris"]
for i, name in enumerate(names):
    print(i, name)

for i, name in enumerate(names, start=1):
    print(f"{i}. {name}")
Lab 1

Run the first loop above. What index does the first item get?

0
Lab 2

Run the second loop. What index does the first item get now?

1
Lab 3

Combine sorted() and enumerate(): sort names alphabetically, then number them starting at 1.

1. Chen, 2. Morris, 3. Rivera
Lab 4

Sort the real USERS fixture by lastName, then print a numbered list of first names using enumerate(..., start=1).

12 numbered lines
Lab 5 · Checkpoint

Write that numbered, sorted list from Lab 4 to a file using with/"w", one "N. Name" line each. Read it back to confirm.

12-line file
Now go practice

Open problems/10_enumerate_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 6, 9.

Concept

Secrets vs. Config: Why a Token Belongs in an Env Var

A hardcoded token in your script file gets committed to source control, shown on anyone's screen, and copy-pasted into chat. An env var lives outside the file entirely — the script reads it at runtime, but the value never touches the code.

Lab 1

Write a script with token = "abc123" hardcoded. In one sentence, what's the risk if this file gets shared or committed to git?

the token leaks to anyone who sees the file
Lab 2

Rewrite it to read the token from os.environ.get("MY_TOKEN") instead. Does the token appear anywhere in the file now?

No
Lab 3

Is a department NAME (like "IT") as sensitive as an API token? Should it also live in an env var? One sentence.

No — it's not a secret, hardcoding it (or a config file) is fine
Lab 4

Name one thing OTHER than an API token that belongs in an env var, not in code.

e.g. a password, a webhook URL, a database connection string
Lab 5

Write a function get_required_env(name) that reads an env var and raises a clear ValueError (with the var name in the message) if it's missing, instead of silently returning None.

Crashes with a clear message when unset
Now go practice

Open problems/11_secrets_vs_config.py and do all 5 problems locally.

Concept

Final Checkpoint: The Zero-Groups Report

The capstone for this whole course: find every ACTIVE user with zero group memberships, sort them by last name, number them, and write the report to a file — without crashing on the fixture's missing department or None lastLogin.

def fetch_zero_group_active(users):
    return [u for u in users if u["status"] == "ACTIVE" and len(u["groups"]) == 0]

def analyze_sorted(users):
    return sorted(users, key=lambda u: u["profile"]["lastName"])

def report(users, path):
    with open(path, "w") as f:
        for i, u in enumerate(users, start=1):
            dept = u["profile"].get("department", "Unknown")
            f.write(f"{i}. {u['profile']['lastName']}, {u['profile']['firstName']} ({dept})\n")

def main():
    zero_group = fetch_zero_group_active(USERS)
    sorted_users = analyze_sorted(zero_group)
    report(sorted_users, "zero_groups_report.txt")

main()
Lab 1

Write fetch_zero_group_active exactly as shown. Call it on USERS. Print len() of the result.

1 (only Dana Morris)
Lab 2

Write analyze_sorted and report exactly as shown. Run the full chain via main().

writes zero_groups_report.txt, no crash
Lab 3

Read the report file back. Print its contents.

1. Morris, Dana (IT)
Lab 4

Why doesn't this script crash on Layla's missing department, even though she's ACTIVE? (she just isn't in the zero-groups result)

Layla has 1 group, so she's filtered out before report ever touches her
Lab 5 · Final checkpoint

Extend main() to read the output path from an env var (REPORT_PATH) with a default of "zero_groups_report.txt", instead of hardcoding it. Run it both with and without the env var set.

This is the capstone — take your time. Explain it out loud in three sentences when you're done, interview-style.
Now go practice

Open problems/12_final_checkpoint.py — 5 problems, #5 is the final checkpoint for all of Module 10, and the biggest one in the course so far.

Complete

Module 10 done

why files matter → open() and the handle → with → reading files → writing files → appending → json.load() → sorted(key=) → enumerate() → secrets vs config → the zero-groups report, the capstone script for everything so far.

Run python check.py in problems/ to grade everything.