2. The monthly export returns
You will use an AI coding assistant to produce one small script, understand its steps, and verify its result. Use only public, synthetic, or explicitly approved data. You remain responsible for every command you run and every file the script changes.
Every month, the same export arrives. You correct inconsistent group names, check that every value is usable, calculate a summary, and make something another person can read. In a lab, the rows are synthetic instrument runs and the result is a summary figure. In a company, the rows are fictional monthly records and the result is a summary table.
At Level 2, the blast radius is your team: a bad summary table or figure can be reused by teammates and spread the same error into their work.
You do the transformations by hand because you do not write code. It takes forty minutes, and the exact choices vary from month to month. An AI coding assistant can turn a precise description into a repeatable script, but a polished script is only a proposal. It may misunderstand a column, overwrite the wrong file, drop an invalid row without warning, or report success after testing only its own easy example.
The useful skill is not memorising Python syntax. It is describing the job, limiting what may change, reading the generated steps, running the script on a known input, and using the actual error or output to decide the next small correction.
3. After this you can
- Describe a script through its input, output, transformations, and example row.
- Ask a coding assistant for one scoped change without granting broad access.
- Read generated code well enough to connect each part to your request.
- Verify row counts, known results, output files, and a deliberate failure.
- Repair a failure by returning the exact error and relevant context to the assistant.
4. Prerequisites
T02-L02· Your daily driver: files, projects and memory.- Python 3 in a disposable folder, with permission to run local scripts.
- An organisation-approved coding assistant and model provider.
- A plain-text editor or an editor that can show a complete file diff.
- About 60 minutes for the independent exercise.
Use the synthetic examples below first. Do not paste customer records, participant or patient data, unpublished results, internal source code, production configuration, passwords, API keys, tokens, or .env files into a prompt. Do not run an unfamiliar command against a shared drive or production folder. If Python, the assistant, or the data is not approved, complete the review on paper and stop before execution.
5. The idea in one page
A coding assistant needs a testable job, not just a wish such as clean my export. Write a small contract with seven parts:
Outcome: what a correct result lets a person do
Input: exact filename, columns, and one representative row
Transform: the ordered changes to each row
Output: exact filenames and meanings
Scope: files the assistant may create or edit
Out of scope: files, systems, and behaviours that must not change
Proof: known answers and failure cases you will check
For this book, the contract is intentionally narrow:
Outcome: turn one CSV export into a grouped summary table and simple figure.
Input: input.csv with record_id,group,value; value is non-negative.
Transform: trim fields, normalize group names to lowercase, reject any invalid row,
then calculate count and mean value by group.
Output: a new relative directory containing summary.csv and summary.svg.
Scope: create monthly_summary.py only; write only the two named output files at run time.
Out of scope: network calls, package installation, credentials, deletion, input edits,
other repository files, and extra features.
Proof: preserve all four known rows; expected counts and means must match; one bad row
must stop with its line number instead of disappearing; unsafe paths must stop before
an output directory or file is created.
This contract separates instructions from permissions. A request not to read secrets is not access control. Open only the disposable project, keep sensitive files elsewhere, start in read-only or plan mode, and deny unnecessary network, shell, and path access. Approve only commands you understand.
The named assistants differ mainly in how you supervise the same loop. Choose an organisation-approved interface after defining the contract, not before:
| Interface | Examples from the course catalogue | Useful when | Check before use |
|---|---|---|---|
| Editor-integrated assistant | Cursor, GitHub Copilot, Devin Desktop (formerly Windsurf), Antigravity | You want the request, changed file, and diff together | Which files and commands it may access, and whether plan-only review is available |
| Terminal or repository assistant | Claude Code, Codex, Aider, opencode, jcode | You can review proposed commands and the complete patch | Working-directory boundary, approval rules, network access, and the exact diff |
| Additional input layer | opencode + voice | Spoken input removes a typing barrier | The transcript before execution; voice does not broaden permission |
This is a workflow comparison, not a claim that the products have identical controls. Product behaviour changes; confirm current permissions and data handling in primary documentation and your organisation's policy. The proof in this book stays the same whichever approved interface you use.
Read generated code by tracing data rather than decoding every symbol. Find these landmarks:
- Imports: what capabilities enter the script? Standard-library modules are enough here.
- Input: which path is opened, and is it opened for reading?
- Validation: what causes a clear stop rather than a silently missing row?
- Transformation: where are group names normalised and means calculated?
- Output: which exact files are opened for writing?
- Entry point: what runs when you type the command?
If you cannot state what each landmark should do, pause. An assistant cannot resolve an undefined business or scientific rule by guessing. Ask the data owner whether Control, control, and CONTROL are truly the same group, whether negative values are valid, and how missing values should be handled before requesting code.
6. The worked example: one script, two skins
Create an empty disposable folder containing only input.csv. Open that folder in your approved coding assistant. Start in Ask, Plan, or another read-only mode and submit the contract from section 5 with this final instruction:
First restate the input, transformations, output files, forbidden actions, and proof.
Then propose the smallest plan without editing or running anything. If a requirement is
ambiguous, ask one question rather than choosing a rule. After I approve the plan, create
monthly_summary.py only. Use the Python standard library and show the complete diff.
Reject a plan that installs a package, sends data over the network, scans unrelated folders, edits input.csv, or creates more source files. After the plan matches the contract, allow the assistant to create only monthly_summary.py. A suitable result is:
"""Create a validated grouped summary from one CSV.
Input: read the relative CSV named on the command line without changing it.
Validation: stop on wrong headers, malformed rows, blanks, duplicates, or invalid values.
Normalisation: trim fields and case-fold group names.
Summary: calculate a count and decimal mean for every group.
Outputs: create one new relative directory containing summary.csv and summary.svg.
Command: python monthly_summary.py INPUT.csv OUTPUT_DIRECTORY
Known test: the four-row Lab fixture yields control,2,12.00 and treatment,2,21.00;
an invalid value or duplicate ID on line 5 stops before the output directory is created.
"""
import csv
import sys
from decimal import Decimal, InvalidOperation
from html import escape
from pathlib import Path
def load_rows(input_path):
with input_path.open(newline="", encoding="utf-8") as source:
reader = csv.DictReader(source, strict=True)
expected = ["record_id", "group", "value"]
if reader.fieldnames != expected:
raise ValueError(f"Headers must be exactly: {', '.join(expected)}")
rows = []
seen_ids = set()
try:
for line_number, row in enumerate(reader, start=2):
if None in row:
raise ValueError(f"Line {line_number}: too many fields")
record_id = (row["record_id"] or "").strip()
group = " ".join((row["group"] or "").split()).casefold()
raw_value = (row["value"] or "").strip()
if not record_id or not group or not raw_value:
raise ValueError(f"Line {line_number}: blank required value")
if record_id in seen_ids:
raise ValueError(f"Line {line_number}: duplicate record_id {record_id}")
try:
value = Decimal(raw_value)
except InvalidOperation as error:
raise ValueError(f"Line {line_number}: value is not a number") from error
if not value.is_finite() or value < 0:
raise ValueError(f"Line {line_number}: value must be finite and non-negative")
seen_ids.add(record_id)
rows.append((group, value))
except csv.Error as error:
raise ValueError(f"Line {reader.line_num}: malformed CSV") from error
if not rows:
raise ValueError("Input contains no data rows")
return rows
def summarize(rows):
totals = {}
for group, value in rows:
count, total = totals.get(group, (0, Decimal("0")))
totals[group] = (count + 1, total + value)
return [(group, count, total / count) for group, (count, total) in sorted(totals.items())]
def csv_safe(value):
text = str(value)
if text.startswith(("=", "+", "-", "@")):
return "'" + text
return text
def resolve_paths(input_argument, output_argument):
project_dir = Path.cwd().resolve(strict=True)
def require_relative(argument, label):
path = Path(argument)
if path.is_absolute() or path.anchor:
raise ValueError(f"{label} must be relative to the disposable project directory")
if ".." in path.parts:
raise ValueError(f"{label} must not contain parent traversal")
return path
input_relative = require_relative(input_argument, "Input path")
output_relative = require_relative(output_argument, "Output path")
try:
input_path = (project_dir / input_relative).resolve(strict=True)
except FileNotFoundError as error:
raise ValueError("Input path does not exist") from error
output_dir = (project_dir / output_relative).resolve(strict=False)
for path, label in ((input_path, "Input path"), (output_dir, "Output path")):
try:
path.relative_to(project_dir)
except ValueError as error:
raise ValueError(f"{label} escapes the disposable project directory") from error
if not input_path.is_file():
raise ValueError("Input path must be a file")
if output_dir.exists():
raise ValueError("Output path already exists; choose a new relative directory")
return input_path, output_dir
def write_outputs(summary, output_dir):
output_dir.mkdir(parents=True, exist_ok=False)
with (output_dir / "summary.csv").open("x", newline="", encoding="utf-8") as target:
writer = csv.writer(target)
writer.writerow(["group", "count", "mean_value"])
for group, count, mean in summary:
fields = [group, count, f"{mean:.2f}"]
writer.writerow([csv_safe(field) for field in fields])
max_mean = max(mean for _, _, mean in summary) or Decimal("1")
bars = []
for index, (group, count, mean) in enumerate(summary):
y = 30 + index * 42
width = int(mean / max_mean * 400)
label = escape(f"{group}: {mean:.2f} (n={count})")
bars.append(f'<text x="10" y="{y + 16}">{label}</text>')
bars.append(f'<rect x="210" y="{y}" width="{width}" height="22" fill="#315f72"/>')
height = 50 + len(summary) * 42
svg = (
f'<svg xmlns="http://www.w3.org/2000/svg" width="640" height="{height}" '
'role="img" aria-labelledby="summary-title summary-desc">'
'<title id="summary-title">Mean value by group</title>'
'<desc id="summary-desc">Horizontal bars compare each group mean; '
'text labels give the mean and record count.</desc>'
'<style>text { font: 14px sans-serif; }</style>' + "".join(bars) + "</svg>"
)
with (output_dir / "summary.svg").open("x", encoding="utf-8") as target:
target.write(svg)
def main():
if len(sys.argv) != 3:
raise SystemExit("Usage: python monthly_summary.py INPUT.csv OUTPUT_DIRECTORY")
try:
input_path, output_dir = resolve_paths(sys.argv[1], sys.argv[2])
rows = load_rows(input_path)
summary = summarize(rows)
write_outputs(summary, output_dir)
except (OSError, ValueError) as error:
raise SystemExit(f"Error: {error}") from error
print(f"Processed {len(rows)} rows into {len(summary)} groups.")
if __name__ == "__main__":
main()
Before running it, inspect the changed-file list; it must contain only monthly_summary.py. Search the diff for http, requests, subprocess, remove, unlink, .env, and unexpected absolute paths. Any would contradict this contract. Trace the functions: resolve_paths anchors both arguments to the current disposable project, load_rows validates, summarize calculates, write_outputs creates a new destination and writes the table and figure, and main connects the steps. resolve(strict=True) follows the existing input, while resolve(strict=False) follows any existing output ancestors; the containment check therefore rejects a symbolic link that leads outside the project. Parent traversal is rejected directly. Exclusive creation (exist_ok=False and "x") refuses reuse rather than overwriting.
Lab skin: instrument export and summary figure
Put this synthetic content in input.csv:
record_id,group,value
RUN-101,Control,10
RUN-102, control ,14
RUN-103,Treatment,20
RUN-104,TREATMENT,22
From the disposable folder, run:
python monthly_summary.py input.csv output
The terminal must say Processed 4 rows into 2 groups. Open output/summary.csv. It must contain control,2,12.00 and treatment,2,21.00. Open output/summary.svg in a browser. It must show two labelled bars, with treatment longer than control. Its image semantics, title, description, and visible labels support assistive technology. Check the table values yourself; the picture is not independent proof.
Now change only RUN-104's value to not-recorded and run with the new destination lab-invalid. The script must stop with Line 5: value is not a number. It must not claim that three rows were processed or create lab-invalid. Restore 22 and run with lab-final to return to the known result. A destination is single-use: never delete or overwrite an earlier result merely to make a command pass. For real lab use, a domain owner must decide whether these columns, units, grouping rules, and mean are scientifically appropriate before any approved data is processed.
Company skin: monthly export and summary table
Use the same script with this synthetic input.csv:
record_id,group,value
INV-201,North,120
INV-202, north ,80
INV-203,South,75
INV-204,SOUTH,125
Run with a new destination such as company-output. The terminal must report four rows and two groups. The summary table must contain north,2,100.00 and south,2,100.00; the figure must show equal-length bars. Replace INV-204 with duplicate ID INV-203 and run with company-duplicate. The script must identify line 5 as a duplicate instead of counting it twice or creating that destination. Restore the unique ID and run with company-final.
For real company use, the data owner must define whether group case should be merged, whether values may be negative, and whether a mean is the right measure. The script does not send, update, approve, or delete anything. Do not add those actions as a convenient follow-up to this first local transformation.
Reproduce the output-safety checks
Spreadsheet programs may interpret CSV cells beginning with =, +, -, or @ as formulas or commands. csv_safe prefixes an apostrophe before writing such fields to summary.csv; calculations and SVG labels remain unchanged. Temporarily replace input.csv with this synthetic test:
record_id,group,value
SAFE-1,=2+2,1
SAFE-2,+cmd,2
SAFE-3,-10,3
SAFE-4,@sum,4
Run python monthly_summary.py input.csv formula-output, then these standard-library assertions:
python -c "import csv; rows=list(csv.DictReader(open('formula-output/summary.csv', encoding='utf-8'))); assert len(rows)==4 and all(row['group'].startswith(chr(39)) for row in rows)"
python -c "import xml.etree.ElementTree as ET; root=ET.parse('formula-output/summary.svg').getroot(); ns={'s':'http://www.w3.org/2000/svg'}; assert root.attrib.get('role')=='img' and root.attrib.get('aria-labelledby')=='summary-title summary-desc' and root.find('s:title',ns) is not None and root.find('s:desc',ns) is not None"
Both must finish without AssertionError. Inspect the CSV as plain text and confirm each group starts with an apostrophe. Restore a known input and run it into a new final destination before keeping the artifact.
To reproduce the path rejections, save the valid synthetic Lab CSV first. In PowerShell, run the following once from the disposable project that contains monthly_summary.py; path-test-root must not already exist. The harness makes a fresh nested disposable project so that its sibling is unambiguously outside the script's allowed root. It tests absolute input and output, parent traversal, an existing destination, and input and output-directory symbolic-link escapes. Creating symbolic links may require an approved terminal setting; if that setup command is denied, ask an administrator to run this synthetic check rather than weakening the script.
New-Item -ItemType Directory -Path path-test-root
New-Item -ItemType Directory -Path path-test-root/project, path-test-root/outside
Copy-Item monthly_summary.py path-test-root/project/monthly_summary.py
Copy-Item input.csv path-test-root/project/input.csv
Copy-Item input.csv path-test-root/outside/input.csv
Push-Location path-test-root/project
New-Item -ItemType Directory -Path already-there
New-Item -ItemType SymbolicLink -Path linked-input.csv -Target ../outside/input.csv
New-Item -ItemType SymbolicLink -Path outside-link -Target ../outside
$cases = @(
@((Resolve-Path input.csv).Path, 'absolute-input-output'),
@('input.csv', (Join-Path (Get-Location) 'absolute-output')),
@('../outside/input.csv', 'traversal-input-output'),
@('input.csv', '../outside/traversal-output'),
@('input.csv', 'already-there'),
@('linked-input.csv', 'linked-input-output'),
@('input.csv', 'outside-link/new-output')
)
foreach ($case in $cases) {
& python ./monthly_summary.py $case[0] $case[1] 2>$null
if ($LASTEXITCODE -eq 0) { throw "Unsafe path was accepted: $($case -join ', ')" }
}
if (Test-Path ../outside/traversal-output) { throw 'Traversal output was created' }
if (Test-Path ../outside/new-output) { throw 'Symlink-escape output was created' }
Pop-Location
The block must finish without throw. already-there must remain untouched, and neither outside output may exist. These checks use only synthetic data; do not substitute a real export or an important destination.
Repair from evidence, not from frustration
If your run fails unexpectedly, do not send it does not work. Copy the exact command, exact error, relevant synthetic header and example row, expected behaviour, and unchanged scope. For example:
Command: python monthly_summary.py input.csv output
Error: Line 5: value is not a number
Relevant row: RUN-104,TREATMENT,22 units
Expected rule: reject values containing units; do not extract 22 automatically.
Scope remains monthly_summary.py only. Explain the cause and propose one minimal change.
Do not edit until I approve the plan.
In this case, the current script already follows the expected rule, so no code change is needed; the row must be corrected by its owner. A good assistant should say that. If a code change is justified, review the new diff from the original contract and rerun every known-input and deliberate-failure check, not just the case that prompted the repair.
7. What goes wrong
You run code you cannot describe
Symptom: you know the command but cannot say which files it reads, writes, or transforms.
Fix: locate imports, input, validation, transformation, output, and entry point. Stop if you cannot connect them to the contract.
A credential enters the prompt
Symptom: the assistant asks for an API key or .env file even though the task is a local CSV transformation.
Fix: refuse the request. Keep credentials outside the project and use permissions to deny sensitive paths. This script needs no network or account.
There is no known-input test
Symptom: the output looks plausible, but nobody knows the correct answer in advance.
Fix: calculate a tiny synthetic case by hand, including case and whitespace variation, then compare every count and mean.
Invalid rows disappear silently
Symptom: the input has four rows, the output reflects three, and no error explains the loss.
Fix: require all-or-stop validation with a line number. Compare the processed-row message with the input row count.
The assistant widens the change
Symptom: a CSV summary request adds a dependency, edits configuration, or proposes uploading the export.
Fix: reject the patch, return to the one-file scope, and approve only the local proof command.
Yesterday's working script is gone
Symptom: repeated assistant edits leave no identifiable last-known-good version.
Fix: keep the script in a disposable Git repository or make an approved versioned copy before each accepted change. Record the command and known result with the version.
8. Do it yourself: one verified script in 60 minutes
Minutes 0-8: choose the Lab or Company skin and one synthetic, public, or explicitly approved CSV. Write the outcome, exact headers, one example row, three transformations, two output filenames, and all forbidden actions. Decide the expected treatment of blanks, duplicates, invalid numbers, and group-name case.
Minutes 8-15: create a disposable folder or branch. Keep secrets and unrelated files outside it. Ask the approved coding assistant to restate the contract and produce a plan without editing. Reject ambiguity and scope expansion.
Minutes 15-28: permit one script file only. Ask for standard-library code and a complete diff. Trace imports, input, validation, transformation, output, and entry point. Explain each before running it.
Minutes 28-38: run the script on at least four synthetic rows whose grouped counts and means you calculated by hand. Compare the terminal row count, every summary row, both output filenames, and the figure labels with your expected result.
Minutes 38-47: create one invalid number and one duplicate ID. Run each separately with a new output name. Confirm that both stop with the correct line number and neither is silently excluded. Restore the valid known input, then reproduce the absolute-path, traversal, symbolic-link escape, and existing-destination rejections.
Minutes 47-54: report the exact command, error or wrong value, relevant row, expected behaviour, and unchanged scope. Approve a minimal repair, recheck the diff, and repeat all tests.
Minutes 54-60: preserve the final script and put one-line explanations for its input, validation, normalisation, summary, outputs, and command in its top docstring. Record the known test values and final successful command in that same docstring. Do not add deployment, scheduling, upload, email, or database access.
9. Exit check
Deliver exactly one artifact: one working script annotated with a one-line explanation of each step.
It passes when another person can run its recorded command against the synthetic test fixture; identify the exact files read and written; match every code step to an explanation; reproduce the known counts and means; see malformed, invalid, and duplicate rows stop with line numbers; reproduce rejection of absolute paths, traversal, symbolic-link escapes, and an existing output destination before any output is created; and confirm that no credential, network call, package installation, input edit, silent row drop, overwrite, or unrelated file change is present. The explanations, command, and expected test results must be embedded in the script's top docstring. The test CSV is a fixture used to evaluate the artifact, not a second delivered artifact.
10. Rule to remember
If you cannot say what it should do, it cannot either.
11. Further reading & tools
- Taught:
T02-L02· Your daily driver: files, projects and memory - keeps project context bounded and separates current instructions from stale material. - Catalogued:
T08-L03· Spec-driven development - continues from this bounded script to acceptance criteria and reviewable implementation contracts. - Catalogued:
T10-L02· Clean a messy export - develops repeatable cleaning, validation, and transformation records for recurring exports. - Taught: AI coding assistants - practises the product-neutral task contract, path boundary, diff review, permissions, and focused proof used here.
- Taught: Cursor - local course workflow for planning, a limited working set, diff review, and focused testing.
- Taught: GitHub Copilot - local course workflow for Ask, Plan, and Edit modes with human review.
- Taught: Claude Code - local course workflow for plan-first changes, permissions, diffs, and tests.
- Catalogued: Codex, Aider, opencode, jcode, Devin Desktop (formerly Windsurf), Antigravity, and opencode + voice - alternative interfaces for the same bounded loop; confirm current product controls before use.
- Primary: Python
csvdocumentation (opens in a new tab) - primary language documentation for reading and writing the CSV files in the example. - Primary: Python
decimaldocumentation (opens in a new tab) - primary language documentation for decimal values and invalid-number handling. - Primary: GitHub responsible use of Copilot (opens in a new tab) - primary vendor guidance on reviewing and validating generated suggestions.
- Primary: Cursor security (opens in a new tab) - primary vendor documentation for current privacy and security controls; verify the applicable plan and settings.
- Primary: Anthropic Claude Code permissions (opens in a new tab) - primary vendor documentation for allow, ask, and deny controls.
- Primary: OpenAI Codex permissions (opens in a new tab) - primary vendor documentation for sandbox and approval controls.
- Primary: OpenCode permissions (opens in a new tab) - primary project documentation for controlling actions and paths.
- Catalogued: Tools index - compare tools only after defining the task, approval boundary, data policy, and proof.