Introduction
A community dinner needs an invitation for every guest. You will turn a CSV guest list, JSON event details and a prepared HTML design into a new batch of personal invitation files. Two guests with the same name must still receive separate files, and names such as Zoë, "Z" & <Friends> must appear as text.
You need Python strings, dictionaries, loops and functions, plus basic terminal use. This lab explains file reads, CSV records, templates, HTML text escaping and new output directories. The CLI and page design are provided; your work is the field mapping, complete validation and per-guest generation. You will not build CSS, send email or make PDFs. Estimated time: 55–75 minutes, including reading and ordinary mistakes.
Open Your First Invitation
In this step, you will generate one normal invitation and connect the page to its input fields. Open the WebIDE tab. First choose File → Open Folder... from its application menu (open the top-left menu button if the menu bar is collapsed), enter /home/labex/project/invitations, and confirm OK. If a workspace trust prompt appears, trust this prepared lab folder. Wait until the folder is open, then use Terminal → New Terminal and wait for the labex shell prompt. All shell commands go in this integrated terminal; Invitations is the separate platform tab for actual output files. The prepared workspace is independent of earlier labs.
cd changes the current directory. An absolute path starts at /; relative paths are resolved from the current directory. If a later command cannot find a file after you move elsewhere, return here with the same command:
cd /home/labex/project/invitations
cat prints a text file. Read the event details:
cat event.json
JSON uses braces for objects and double quotes for string keys and values. These four event values become the invitation's title, date, time and venue. The ready-to-run one.py example already maps them and a single guest into the supplied design. python3 runs a Python file. Generate the example into the new batches/first directory:
python3 one.py
Expected output:
Created batch: batches/first
Switch from the WebIDE platform tab to Invitations. The invitation browser is already running. Refresh the preview and find first/g001.html. Click Open invitation, which opens the actual HTML file in a new browser tab. Read Maya Patel, The Long Table, the time and venue. Close that browser tab to return to Invitations. Download HTML saves the same file for use outside the lab.

The greeting identifies the guest; the two lower sections show the event time and venue.
Find the input-to-template relationship
Return to WebIDE, keeping the same workspace and integrated terminal. The left Explorer should show one.py, generate.py, rendering.py, batch.py, invitation.html and the input files. If Explorer is hidden, choose View → Explorer. Opening a folder does not install or restart anything.
Click one.py. Trace event into values, then safe, then template.substitute(safe). The dictionary maps the guest's name/message and four event values into six text slots. The safe comprehension visits each key/value pair and calls escape(value, quote=True) before substitution. This normal example is complete for Maya only; it does not use the incomplete reusable renderer or batch writer.
Open invitation.html as another editor tab and locate its six data-field markers and $name, $message, $title, $date, $time, $venue placeholders. Preserve this template. You will make the reusable function choose the right values for any guest and event, rather than fill this one invitation with fixed answers.
Use Edit → Find or Ctrl+F (Cmd+F on macOS) to locate a function/variable in the active file; press Escape to return to editing. Before editing, check the filename on the editor tab. Save with File → Save or Ctrl+S (Cmd+S on macOS). A dot on the tab indicates changes not yet saved. WebIDE may have File → Auto Save enabled, so it may save the file and remove the dot automatically. Use Save after editing anyway, and check that no unsaved dot remains before running Python in the integrated terminal; a dot need not remain visible until you click Save. Invitations already runs the real file viewer; do not use Go Live.
The preview reads only files that actually exist in batches. It does not generate missing invitations. The earlier batches/archive is retained as old work. Your final program will create new batches, keeping these files and all inputs intact. Return to WebIDE for the next step.
Map CSV Records to HTML Text
In this step, you will make the reusable renderer put each guest value into the right text slot. Return to WebIDE and its same integrated terminal; relative paths below begin in /home/labex/project/invitations.
Read records rather than splitting commas
CSV is a table format. Its first row contains field names. A comma separates fields unless it occurs inside a quoted field; a quote inside a quoted field is doubled. Therefore, split(",") cannot correctly read every row. Read the supplied list:
cat guests.csv
There are two Alex Chen records with different guest_id values. The last guest’s name includes a comma, quotes, & and angle brackets. These are legal name characters. A stable ID identifies a person; the display name does not.
The prepared generate.py reads these records. Read it before changing the renderer:
cat generate.py
with ... as source opens a file and closes it after the indented block. encoding="utf-8" preserves Unicode characters. newline="" lets the CSV reader handle record boundaries itself. csv.DictReader(source) uses the header to make one dictionary per record; list(...) reads the records into a list. For example, guest["name"] is one complete name, including a comma if it belongs to that name. json.loads(...) converts event JSON text into a dictionary. These readers and the CLI are already implemented.
Keep CSV quoting and HTML escaping separate
An HTML page uses <...> for tags and &...; for character references. CSV quotes protect the table structure; they do not stop a name such as <Friends> from being interpreted as HTML. html.escape(value, quote=True) converts special characters to references that a browser displays as the original text. Here every supplied value goes into an HTML text slot. This is not a recipe for inserting arbitrary JavaScript, CSS or URLs.
Template uses $name, $title and the other $field placeholders in invitation.html. substitute(mapping) replaces each placeholder with its corresponding dictionary value and refuses a missing mapping. The template's six body markers, data-field="name", message, title, date, time and venue, identify the semantic text slots. Keep one of each, containing the corresponding text. Keep the supplied input template unchanged. Equivalent tag choices, layout and entity spellings in generated output are accepted; a name in the page's <title> alone does not fill its body slot.
Edit the existing renderer
Open rendering.py in Explorer and find render_guest. Its current return inserts values without HTML escaping. Predict what <Friends> would do in an HTML text slot: the browser can interpret it as markup. CSV quoting has already protected the record boundaries; HTML is a different boundary.
Keep the function signature. Add the standard-library import above it:
from html import escape
Inside the function, replace the old comment and direct return with this explicit mapping. Obtain name and message from the guest; obtain the four event fields from the event:
values = {"name": guest["name"], "message": guest["message"],
"title": event["title"], "date": event["date"],
"time": event["time"], "venue": event["venue"]}
The first line begins with four spaces inside the function. Continuation lines stay within the braces. Complete the return using a dictionary comprehension, which visits each key/value pair from values.items(). Preserve the keys and escape each text value once before substitution:
return template.substitute({key: escape(value, quote=True)
for key, value in values.items()})

The selected file is rendering.py inside INVITATIONS. Keep the function signature; the local edits are the six-field values mapping and the escaped return. The saved tab has no unsaved dot; Auto Save may already have saved the edit.
Keep the surrounding quotes, braces and parentheses. Save the file and, if an unsaved dot is visible, check that it disappears; Auto Save may already have saved the edit. If Python later reports a syntax/indentation error, read its filename and line number, reopen that file, and correct only the affected line and nearby spaces/brackets. Edit → Undo can undo a recent edit; save the correction before rerunning. A missing-file error instead calls for the earlier cd and running ls in the integrated terminal; ls lists the file and directory names in the current directory. Preserve earlier outputs; do not reset or rerun setup.
Observe text before fixing batch identity
Return to the integrated terminal. The CLI takes four positional paths: guest CSV, event JSON, template HTML, then a new output directory. Run an explicitly incomplete draft:
python3 generate.py guests.csv event.json invitation.html batches/text-draft
Created batch: batches/text-draft
Switch to Invitations, refresh, and open the file under text-draft whose filename begins with Zoë. The greeting must retain the comma, quotes, ë, ampersand and literal <Friends>, with that guest's own message. Close the browser tab and return to WebIDE.
The batch writer still uses display names as filenames: the two Alex records can overwrite each other. Correct text alone is not a complete batch. Keep this draft so the next step can explain and repair the identity problem. This step's check evaluates only the renderer against an independent guest/event, not that unfinished batch writer.
The function returns HTML text; it does not create files. Local helpers may be top-level .py files beside generate.py; package subdirectories are outside this exercise’s scope. Escaping happens once, before substitution, so an ampersand appears as & in the browser rather than as a visible &. You have now separated the invitation's design from its personalized text.
Validate Everyone Before Creating a Batch
In this step, you will improve the existing batch writer in three edits: use guest IDs, validate every record, then prepare every page before creating output. Continue in WebIDE and its integrated terminal. Only batch.py changes here; keep your completed renderer.
Read the actual draft and its output:
cat batch.py
ls batches/text-draft
There are only two HTML files for three CSV records: the two Alex Chen records used one path. This was a known incomplete draft, not a valid deliverable. The separate batches/first and batches/archive remain intact.
Give each guest an independent file
Open batch.py in Explorer. Preserve the two imports and function signature. Remove the obsolete draft comment. Keep output = Path(output), but change output.mkdir(exist_ok=True) to:
output.mkdir() ## An existing batch is always refused.
Path represents the output directory. Without exist_ok=True, mkdir() refuses an existing path rather than reusing earlier work. Inside the existing for guest in guests: loop, replace only the file-writing statement with:
(output / f"{guest['guest_id']}.html").write_text(
render_guest(guest, event, template), encoding="utf-8")
The / operator joins paths; the f-string inserts the stable ID into a filename. The statement begins with eight spaces inside the loop. This first edit assumes valid supplied IDs; validation follows. Save, then run:
python3 generate.py guests.csv event.json invitation.html batches/id-draft
ls batches/id-draft
You should see g101.html, g102.html and g103.html. Switch to Invitations, refresh, and open id-draft/g101.html and id-draft/g102.html: same name, separate messages. Close the invitation browser tabs and return to WebIDE. Deliberately repeat the completed path:
python3 generate.py guests.csv event.json invitation.html batches/id-draft
This is an expected failure beginning Cannot generate invitations: with an existing-file error. The earlier files remain unchanged. This intermediate still lacks input validation; it is not ready for arbitrary records.
Validate the entire list before output
Required guest fields are guest_id, name, message; event fields are title, date, time, venue. Each must be nonblank text. IDs contain only lowercase ASCII letters, digits and hyphens and must be unique. Names and messages remain unrestricted UTF-8 text. Keep all original inputs and batches unchanged.
isinstance(value, str) checks for text. .get(field) returns None for a missing key. strip() detects whitespace-only text without changing the value later displayed. raise ValueError(...) stops the function; the supplied CLI prints the explanation and exits nonzero. A set() stores IDs, in checks membership, seen.add(...) remembers one, and all(...) checks every character.
Insert these validation loops before output = Path(output). Keep the existing output and writing loop below them for now:
for field in ("title", "date", "time", "venue"):
if not isinstance(event.get(field), str) or not event[field].strip():
raise ValueError(f"Missing event field: {field}")
if not guests:
raise ValueError("The guest list is empty")
seen = set()
for guest in guests:
for field in ("guest_id", "name", "message"):
if not isinstance(guest.get(field), str) or not guest[field].strip():
raise ValueError(f"Missing guest field: {field}")
guest_id = guest["guest_id"]
if not all(character in "abcdefghijklmnopqrstuvwxyz0123456789-" for character in guest_id):
raise ValueError("Guest IDs must use lowercase letters, digits, or hyphens")
if guest_id in seen:
raise ValueError(f"Duplicate guest ID: {guest_id}")
seen.add(guest_id)
The function body uses four spaces, the guest-loop body eight, and the inner field check twelve. Every guest must be validated before reaching output.mkdir(). Save, then run a normal input into a new draft:
python3 generate.py guests.csv event.json invitation.html batches/checked-draft
Read the prepared failures:
cat invalid-duplicate.csv invalid-missing.csv
The duplicate repeats g101; the missing-message input has an invalid later record. The following commands intentionally fail with Duplicate guest ID: g101 and Missing guest field: message respectively:
python3 generate.py invalid-duplicate.csv event.json invitation.html batches/duplicate-draft
python3 generate.py invalid-missing.csv event.json invitation.html batches/missing-draft
ls batches
duplicate-draft and missing-draft must be absent: a valid earlier record cannot justify output before validating a later record. Keep successful drafts and originals.
Prepare every page before creating the directory
Validation is not the last possible input failure. Template.substitute refuses an unknown placeholder. An unrenderable page should not leave even an empty batch directory.
Create a separate, intentionally unfillable template copy. read_text reads the original without changing it; write_text creates unfillable.html. The short here-document sends literal Python to python3; include its final PY. Quoting <<'PY' prevents shell expansion of $unavailable, which belongs to the template text. The \n characters add newlines to that text.
python3 - <<'PY'
from pathlib import Path
original = Path('invitation.html').read_text(encoding='utf-8')
Path('unfillable.html').write_text(original + '\n<p>$unavailable</p>\n', encoding='utf-8')
PY
Run this intentional failure with the current intermediate writer:
python3 generate.py guests.csv event.json unfillable.html batches/template-draft
It reports Cannot generate invitations: 'unavailable'. Rendering fails on the first guest, after the directory has already been created. This fixture leaves an empty directory, not partially rendered pages. Inspect it:
ls -A batches/template-draft
There should be no filename output. -A includes hidden names other than . and ... Keep this failed draft; it is not a complete batch.
Return to the batch.py editor. Immediately after seen = set(), add pages = []. At the end of the validation loop, immediately after seen.add(guest_id), add:
pages.append((guest_id, render_guest(guest, event, template)))
The list stores each ID and its fully rendered HTML. Any rendering error now happens before the output directory is created. Keep output = Path(output) and output.mkdir() after this preparation loop. Replace only the final immediate-render writing loop with:
for guest_id, page in pages:
(output / f"{guest_id}.html").write_text(page, encoding="utf-8")

In batch.py, pages.append(...) stays inside the guest loop. output.mkdir() and the final writing loop start after that loop, at four-space function indentation. This is the boundary that prevents a template error from creating a batch directory.
The loop unpacks each saved pair without rendering again. Read the order: validate event, validate guests and prepare every page, create a new directory, then write. Save. This protects against the input/rendering failures taught here, not disk failures, sudden power loss or concurrent writers.
Repeat the unfillable-template test with a new output name:
python3 generate.py guests.csv event.json unfillable.html batches/rejected-template
The same error is expected, but no directory should exist this time. This read-only check uses .exists() to ask about the path and prints a Boolean:
python3 - <<'PY'
from pathlib import Path
print('Rejected template batch exists:', Path('batches/rejected-template').exists())
PY
Rejected template batch exists: False
Finally use the original complete template to make the real evening batch:
python3 generate.py guests.csv event.json invitation.html batches/evening
Created batch: batches/evening
ls batches/evening
The three ID-named files now contain correct individual text. The final program handles identity, whole-input validation and complete rendering before output. Run the step check only now. Keep first, archive, every draft and all original inputs intact.
Reuse the Generator and Keep Earlier Work
In this step, you will use the same program for another event and practice ordinary error recovery. Begin in WebIDE, using the same integrated terminal. Each command below is separate, so an expected failure does not prevent you from continuing.
First deliberately repeat the completed output path. This is expected to fail: mkdir() refuses an existing batch. The message begins Cannot generate invitations: and includes an existing-file error. The previous batch must remain unchanged.
python3 generate.py guests.csv event.json invitation.html batches/evening
Read the prepared duplicate-ID example. The second guest has the first guest's ID:
cat invalid-duplicate.csv
Try to create a new batch from it. This is another expected failure, reporting Duplicate guest ID: g101 before any directory is created:
python3 generate.py invalid-duplicate.csv event.json invitation.html batches/rejected-duplicate
The missing-message example also fails intentionally with Missing guest field: message. Even though earlier records are valid, no partial batch should appear:
python3 generate.py invalid-missing.csv event.json invitation.html batches/rejected-missing
A misspelled input path is different: the operating system cannot open the file. The next command intentionally uses guest.csv, which does not exist. Read the error; do not reset the environment:
python3 generate.py guest.csv daytime.json invitation.html batches/daytime
Recover by using the actual guests.csv path. Read the second event, then generate a new batch with the same program and guest list:
cat daytime.json
python3 generate.py guests.csv daytime.json invitation.html batches/daytime
Expected output:
Created batch: batches/daytime
Switch to Invitations and refresh. Open evening/g101.html and evening/g102.html in turn. They share a name but retain their different messages. Open evening/g103.html: the comma, quotes, ë, ampersand and <Friends> must all appear as literal text. Open daytime/g103.html and read its new title, time and venue. Close each invitation's browser tab to return to the list. Use Download HTML for the daytime invitation and open the downloaded file: its personalized text should match the preview.

The name keeps its comma, quotes and angle brackets. They are visible text in the greeting.
The two event batches are independent. The CSV, event files and template remain unchanged, and the old archive is still present. Invalid input, an existing batch and a missing input path have distinct explanations; each can be corrected without destroying earlier invitations.
Summary
You generated real, downloadable HTML invitations from CSV records and JSON event details. You separated stable IDs from display names, mapped fields into template text slots, preserved Unicode and special characters with UTF-8 and HTML escaping, and validated an entire batch before writing. The generator can now create new event batches while preserving its inputs and earlier work.



