Week 2Day 8 of 30120 minutes~81 min of reading

Build — the 20% of the language that shows up in every demo

Python just enough to ship a thin slice

Build enough to be dangerous

Why this matters for a delivery manager

You do not need LeetCode. You need to open a script, see a loop over documents, see a JSON payload, and change a constant without waiting. That is the bar for FDE and solutions interviews that say 'some Python.' Today is that bar, nothing more.

The reason this day exists is not to make you a developer. It is to stop you from treating the repo as a black box you are not allowed to open. A delivery manager who can point at the line that hard-codes a folder path, the line that builds the messages array, and the line that prints the model output is already more useful in a customer workshop than a manager who waits for a screen-share.

You will also leave with a personal rule for secrets and a clear split of labor: what you will write, what you may write, and what an engineer must write. If you blur that split, you either freeze (and look junior) or you start pasting API keys into Slack (and look dangerous).

You will be able to

  • Read a 40-line Python script that calls an API and not get lost
  • Use dicts, lists, functions, and JSON as the data shapes of AI work
  • Know what you will write vs what an engineer will write on an FDE / SE squad
  • Set up a mental model of files, virtualenvs, and secrets so you don't paste keys into Slack

2-hour clock

120:00

Now: Read the language as data shapes · 50m

The 2-hour session

Concepts, in full

This block is a slow read — about an hour with the diagrams. After each concept, write one sentence in notes (what you already do vs what is new) and tick annotated. Do not skim the last concept.

01

The only types you need this month

Python has more types than you will use this month. Ignore the extras on purpose. The palette for AI delivery work is short: str for text, int and float for counts and scores, bool for flags, list for ordered collections, dict for named fields, and None for missing. Classes, decorators, async, generators, and type gymnastics will show up in other people's code. You can learn to recognize them later. You do not need to write them today, and pretending you do is how people stall for three weeks on a syntax course.

str is text. The user question is a str. The system prompt is a str. The SOP page you loaded from disk is a str. The model's answer is a str. Almost every 'document' you care about enters the program as a string and leaves as a string. When someone says 'we pass the policy into the model,' they mean a string (often truncated) went into a dict that went into a JSON body.

int and float are numbers with jobs. Token counts, max_tokens, HTTP status codes, and 'stop after 5 files' are ints. Similarity scores, temperatures, and prices are floats. You will read them in logs and evals. You will almost never invent a new numeric type. If a script asks you to change temperature from 0 to 0.2, you are changing a float. If it asks you to cap the run at 20 documents, you are changing an int.

bool is a gate: True or False. Did the file have any text. Did retrieval return anything. Is this a write. Did the user confirm. You already think in gates — entry criteria, go/no-go, 'is this in scope.' In code the gate is an if. Treat booleans as the RAID version of a flag, not as computer-science trivia.

list is an ordered collection. The messages you send to a chat model are a list. The chunks a retriever returns are a list. The files in a folder are a list. You loop over lists. You index them (first message, top 5 chunks). You slice them (first 200 characters, last 3 turns). When a demo 'goes through the documents,' it is a for-loop over a list of paths or a list of dicts.

dict is named fields. A JSON object becomes a dict in Python. A chat message is a dict with role and content. A retrieved chunk is a dict with text, source, and score. The API payload is a dict. If you can read dicts, you can read AI code. If you cannot, every script will look like noise. Spend your study time here, not on classes.

None means missing. The file was empty. The environment variable was not set. The model refused and the client returned nothing useful. Python will not always shout; it will hand you None and the next line will crash when it tries to call a method on it. Reading a traceback that says 'NoneType has no attribute X' is a delivery skill. It usually means a file, a key, or a field you assumed would be there was not.

When you open a customer script fifteen minutes before a call, you are not hunting for classes. You are hunting for the six shapes. The user question is a str. The messages array is a list of dicts. A retrieved chunk, if this file has already grown into RAG, is a dict with text, source, and score. The cap is an int. The skip-empty test is a bool. The missing key is None, and it will crash on the next attribute access if nobody handled it. Put a mark in the margin at each shape. If you cannot find messages as a list of dicts, you have not found the model call yet. If you cannot find the cap as an int, the run is uncapped, and that is already a review comment you can say out loud without pretending to be an engineer. You do not need to know how Python allocates a list. You need to see the list, name it, and say what happens when it is empty.

What you skip on purpose is as important as what you read. Decorators, async, generics, and a class hierarchy named BaseChain will show up in other people's files this month. You may write 'I do not know this construct; what does it return?' in the margin and still be useful on the call. The junior move is to freeze until you have taken a syntax course. The dangerous move is to nod and then change a line you did not understand. The adult move is to locate the data shapes, the cap, the secret, and the print, and to name the rest as a question for the author. That split — what you can already read, what you will ask — is the same split you use in a vendor workshop when the slide has a word you have not met. You do not fake it. You point at the contract and you ask. After this concept you should be able to do that to a 40-line file without a tutorial open.

Python shapes vs the AI objects you will actually touch
Python shapeEveryday analogueAI object you will see
strA cell of text, a paragraph, a filenameUser question, system prompt, SOP page, model answer
listA column, a stack of files, a queuemessages[], retrieved chunks, file paths, eval rows
dictA form, a JSON object, a row with named columnsChat message, chunk {text, source, score}, API payload
boolA checkbox, a go/no-go flagretrieval_empty, user_confirmed, is_write_tool
int / floatA count, a score, a budgettoken usage, max_tokens, similarity, temperature, price
NoneBlank cell, 'not provided'Missing file, unset API key, empty model content

Python shapes vs the AI objects you will actually touch

Shapes you will see constantly — read the names, not the punctuation
question = "What is the travel cap for contractors?"  # str
messages = [                                          # list of dicts
    {"role": "system", "content": "Cite sources. Do not invent."},
    {"role": "user", "content": question},
]
chunk = {                                             # one dict
    "text": page,            # str  — the passage
    "source": "SOP-14.pdf",  # str  — where it came from
    "score": 0.82,           # float — retriever confidence
}
payload = {                                           # the JSON body
    "model": "frontier",
    "messages": messages,
    "temperature": 0,        # float you almost never raise in prod
    "max_tokens": 800,       # int cap on the answer
}
# If question is missing, you will see None and the next line will break.

02

Functions are named processes

def retrieve(query): is a process with a name, inputs, and an output. You already think this way. A RACI step is a named process. A runbook box is a named process. A function is the same object in a file. Good AI code is a handful of functions with boring names: load_docs, chunk, embed, search, call_model, format_answer. If a file is one giant script with no functions, it will not survive a second use case, and you should say so in the review.

You do not need to write clever functions. You need to see the boundary. load_docs takes a folder and returns a list of dicts. call_model takes a list of messages and returns a string (or a dict with content plus usage). format_answer takes the model output plus the sources and returns what the UI shows. When those boundaries exist, you can change the folder without touching the model call, and you can change the model without touching ingest. That is the same instinct as splitting a workstream so one delay does not freeze the rest.

for item in items: is a loop. You will loop over files, chunks, and eval rows. The body of the loop is 'do the same process to the next thing.' That is batch work. You have run batches. The only new part is that the process might be 'call the model' and therefore cost money and time per item. A loop with no cap is a blank cheque. The line that says if n >= 5: break is a delivery control, not decoration.

if score < 0.4: is a gate. You already love gates — entry criteria, definition of done, 'do not start build until the corpus has an owner.' Put them in code as well as in the runbook. A script that always calls the model, even when the file is empty or retrieval returned nothing, is a script that will hallucinate on your behalf. The gate is the sentence 'if we do not have sources, we do not call, or we call with a refuse instruction.'

Return values are how functions talk to each other. retrieve returns chunks. call_model returns text. If you cannot say in one sentence what a function returns, the function is doing too much or it is named as theatre. Ask the author. 'What comes back from this, and what do we do if it comes back empty?' is a legitimate review question from a delivery lead. You are not being precious. You are refusing to run a process whose output is undefined.

Names are documentation. load_docs is better than do_stuff. call_model is better than run. A function called helper or utils.misc is where bugs go to hide. When you cannot follow a 40-line file, it is often because the names do not match the verbs in the design (ingest, retrieve, generate, cite). Push for names that match the boxes you drew. Engineers who care about operations will agree. Engineers who wanted a demo will bristle. That bristle is information.

You will not memorize syntax for default arguments, type hints, or list comprehensions this month. You will see them. A type hint like def retrieve(query: str) -> list[dict]: is a comment the computer can check — query in, list of dicts out. Read it as a contract. If the body then returns None, the contract is a lie. That is worth a comment in the PR even if you cannot fix it yourself.

A function you cannot name in one verb is a function you cannot put on a RACI. load_docs, chunk, retrieve, call_model, format_answer are verbs a sponsor already understands because they match the boxes on the design. helper, process_data, utils.misc, run2 are where bugs go to hide and where reviews stall. When you have fifteen minutes in a repo, read the def lines first, before the bodies. If the names do not match ingest / retrieve / generate / cite, that mismatch is the first thing you say on the call: 'the file's verbs are not the design's verbs, so I cannot tell which function owns empty retrieval.' Engineers who care about operations will agree. Engineers who wanted a demo will bristle. The bristle is information. You are not asking them to rename for taste. You are asking them to make the process visible, which is the same ask you already make of a runbook that says 'do the thing' in a single box.

Return values are also how you test for empty without reading the whole client. retrieve returns a list. An empty list is a legal return. None is a different object and will crash the next line that tries to iterate. A delivery lead who asks 'what comes back when the folder is empty, and is that a list or None?' is doing a design review, not impersonating a linter. Write that question down. Write the sister question: 'what comes back when the API errors — an exception, an empty string, or a dict with an error field?' If the author cannot answer in a sentence, the function is doing too much or the error path is unowned. Either way you have found the risk before the demo. Caps belong in the same pass: the for-loop that calls the model is a named process with a budget. if n >= 5: break is the budget. If it is missing, you add it before anyone points the script at a share with ten thousand files.

03

JSON is the wire format of this whole industry

json.loads turns a string of text into dicts and lists. json.dumps goes the other way: dicts and lists become a string you can send over HTTP or write to a file. APIs speak JSON. Eval sets are JSONL — one JSON object per line, not one giant array — because you can append a row without rewriting the file and you can stream it. Prompt packs, tool schemas, model responses, usage logs: JSON. Get comfortable staring at braces. The punctuation is not the point. The field names are the point.

A chat completion response is a dict nested inside dicts. You will walk it: data['choices'][0]['message']['content'] is the answer text in the OpenAI-shaped world. data['usage']['prompt_tokens'] is what you were billed on the way in. If a field is missing, Python raises KeyError. That error is not mysterious. It means the response did not contain the key you assumed. Print the keys. Read the error body. Do not restart the laptop.

Errors you will hit in the first week of touching files: trailing commas (JSON forbids them; Python dicts allow them — that mismatch burns everyone), mixing single quotes (JSON requires double quotes for keys and strings), files saved as UTF-16 from Excel, a BOM at the start of the file, and 'pretty' JSON that someone pasted into Slack with a smart quote. This is not 'being bad at coding.' This is the job. The person who can open the file in a text editor, look at the first three characters, and say 'this is not UTF-8 JSON' saves an afternoon.

JSONL is the format you want for evals and traces. Each line is a complete object: id, question, expected_source, tags. You can add a line without breaking the rest. You can count lines and know the set size. You can grep. A .xlsx eval set will be converted, badly, by someone at 11pm. Start in JSONL or CSV, with a schema written in the design: which fields are required, which are optional, what 'empty retrieval' looks like as a row.

When an API call fails, the body is often JSON too: {error: {message, type, code}}. Read it. A 429 with a retry-after field is a rate limit, not 'the model is down.' A 400 with 'maximum context length exceeded' is your retrieval stuffing too much, not a vendor outage. A 401 is the key. Delivery people who skim the status code and ignore the body will file the wrong ticket and burn an engineer for a problem that was a missing header.

Pretty-printing is a reading tool. json.dumps(obj, indent=2) turns a nested dict into something you can review. When a vendor says 'the payload is standard,' dump one. You will find extra fields, missing fields, and a model name you did not agree. Put a redacted sample payload in the design pack. Future-you, and the engineer who was not in the workshop, will need it.

Do not confuse JSON the format with 'the model returning JSON.' You can ask a model to emit JSON (response_format, a schema, a prompt that says 'return only a JSON object with keys X and Y'). Models will still occasionally wrap it in markdown fences, add a preamble, or emit invalid JSON. That is why structured output is a product requirement with a parser and a retry, not a hope. The parser is ten lines. The hope is a production incident.

On a call, walking a nested response is how you look like you belong in the repo. Open the last captured payload — or ask the engineer to dump one with json.dumps(data, indent=2). Find choices, then message, then content. Find usage. If either walk fails, you have found the bug before anyone restarts the laptop. KeyError on 'choices' usually means the error body is a different shape: an error object, not a completion. Read that object. A 400 with maximum context length is retrieval stuffing. A 401 is the key. A 429 is a rate limit with a retry-after you should honor, not a 'model is down' ticket. Delivery people who skim the status code and ignore the JSON body file the wrong ticket and burn an engineer. You already knew to read the error email before you escalated a vendor. Same muscle, braces instead of prose. Pretty-print is a reading tool, not a decoration. Dump one redacted payload into the design pack so the engineer who missed the workshop can see the contract.

Eval sets and traces should be JSONL, one object per line, with a schema you can say in a sentence: id, question, expected_source, tags, and whatever 'empty retrieval' looks like as a row. You can append a line without rewriting the file. You can count lines and know the set size. You can grep. A spreadsheet eval set will be converted, badly, at 11pm, with smart quotes and a BOM. Start in JSONL or CSV. When schema drifts — a new required field, a renamed key — json.loads will still succeed and the next line will KeyError. That is a gift. Fail loud. Do not paper over missing fields with .get(key, ''). Silent defaults are how an eval set quietly stops measuring the thing you thought it measured. The person who can open the file, look at the first three characters, and say 'this is not UTF-8 JSON' saves an afternoon. That person can be you after today, without writing a parser from scratch.

Reading JSON like a reviewer — dump, walk, catch the real error
import json

raw = path.read_text(encoding="utf-8")
try:
    row = json.loads(raw)
except json.JSONDecodeError as err:
    print(f"bad JSON in {path.name}: {err}")
    raise

# eval row contract — fail loud if the schema drifted
question = row["question"]
expected = row["expected_source"]   # KeyError here means the file changed

print(json.dumps(row, indent=2)[:500])  # read it before you trust it

04

Secrets, files, and what 'running it' means

API keys live in environment variables or a secret store. Never in the repo. Never in screenshots. Never in the prompt library you might paste into this app if it contains a key. Never in a Slack thread 'just for today.' You already know 'do not put passwords in the RAID log.' Same instinct, higher blast radius, because a model key in a public gist is a billing incident by morning and a data incident if the key also unlocks logs.

The pattern is boring and that is why it works: the process environment holds MODEL_API_KEY. The script reads os.environ['MODEL_API_KEY'] and dies immediately if it is missing. A .env file can load those values on a laptop if the file is gitignored. A cloud secret manager holds them in production. If you see a key that starts with sk- or a long Bearer token committed in a file, stop the review. Rotating the key is the next action, not 'we will clean it up later.'

A virtual environment is a project-local set of packages so you do not poison the rest of the machine. Python on a laptop is shared. The RAG demo wants package versions that will break the reporting script you ran last year. venv (or uv, or conda — the brand is not the point) creates a folder that holds this project's interpreter and libraries. You activate it, you install from requirements.txt, you work. You do not install into the system Python 'just this once.'

requirements.txt is the bill of materials: package names and usually versions. It is the same object as a vendor list. If the project cannot be recreated from that file, the demo is a snowflake. You do not need to pin the universe today. You do need to notice when someone says 'it works on my machine' and there is no requirements file. That is a delivery risk with a name.

Running it means: a terminal, the right directory, the venv active, the env var set, a command like python summarize.py. It does not mean double-clicking the file. It does not mean pasting the script into ChatGPT and hoping. If you cannot install Python this week, you can still finish the month: trace code, write designs, use an approved chat UI. FDE loops will eventually want a repo. Solutions and delivery leads can often stay at 'I can read it and I have run a notebook once.' Do not fake a local setup in an interview. Say what you have actually done.

Files have encodings and paths. Path('corpus') / 'sop-14.txt' is how you join folders without caring whether the laptop is Windows. read_text(encoding='utf-8') is explicit because the default will bite you. If a document came from a Windows tool, you may need utf-8-sig or even utf-16. When a script 'cannot find the file,' nine times out of ten you are in the wrong working directory. Print Path.cwd(). Print the path you asked for. Then talk.

Your personal rule, written today, should fit on a card: keys in the environment or a vault; .env gitignored; no keys in tickets, screenshots, notebooks checked in, or prompt docs; rotate if it leaked; I do not borrow someone else's key to 'just try.' Put your name under it. This is not theatre. This is the same bar you already hold for customer data.

Fifteen minutes in a customer repo is also a secret scan, and it is the first thing you do, before you admire the loop. Grep the tree for sk-, for Bearer, for api_key =, for a long token in a notebook output cell. Open .gitignore and confirm .env is listed. Open the script and confirm the key is read from os.environ and that a missing key crashes, rather than falling back to a string in the file. If you find a committed key, the next action is rotate, not 'we will clean it up after the workshop.' Say it on the call: 'there is a key in this file; we do not demo until it is rotated and gone from git history.' That sentence is delivery, not pedantry. A model key in a public gist is a billing incident by morning. A key that also unlocks logs is a data incident. You already know not to put passwords in the RAID log. Same instinct, higher blast radius. The scan takes two minutes. Skipping it to look collaborative is how POCs become the story Security tells about you.

Running it, if you run it at all, means a terminal, the project directory, a virtualenv, the env var set, and a command like python summarize.py. It does not mean double-clicking the file. It does not mean pasting the script into a consumer chat UI along with the key. A virtualenv is a project-local set of packages so this RAG demo does not poison the reporting script from last year. requirements.txt is the bill of materials; if it is missing, the demo is a snowflake, and 'it works on my machine' is a delivery risk with a name. If you cannot install Python this week, you can still finish the month: trace code, write designs, use an approved chat UI. Do not fake a local setup in an interview. Say what you have actually done. FDE loops will eventually want a repo. Solutions and delivery leads can often stay at 'I can read it and I have run a notebook once.' The personal rule still holds even if you never run the file: you do not paste the customer's key into this academy, into Slack, or into a screenshot of a 'working' terminal.

Diagram

What has to be true before the script is allowed to run

  1. 01

    Machine + working directory

    You are in the project folder. Python can see the files you named.

  2. 02

    Virtualenv

    This project's packages, not the system Python. requirements.txt is the bill of materials.

  3. 03

    Secrets in the environment

    MODEL_API_KEY set outside the repo. Missing key should crash, not silently skip auth.

  4. 04

    The script

    Reads files, loops, calls, prints. Contains no keys, no production passwords, no customer dumps.

Each layer is a delivery control. Skipping one is how keys leak or demos become unreproducible.

05

What you write vs what an engineer writes

The split is not about intelligence. It is about who owns the failure mode. If the failure is 'the eval set does not represent real questions,' that is you. If the failure is 'the HTTP client does not back off on 429 and we hammered the vendor,' that is an engineer. If you do not name the split, everyone assumes the other person has it, and the demo ships with both holes.

You write, in English, the process the code is supposed to implement. Folder of SOPs in, one summary per file out, skip empty, stop after N, system prompt is this paragraph, temperature 0, do not send the key, print filename and first 200 characters of the answer. That paragraph is a spec. A 40-line script that matches it is a prototype. A service that does it with retries, logging, and auth is engineering. Do not let a sponsor treat the prototype as the service.

You may write the prototype. On an FDE squad that is normal. On a delivery-lead track it is optional and still worth doing once, so you know what '40 lines' feels like. The prototype is allowed to be rude: no retries, no tests, a hardcoded folder, a print instead of a UI. It is not allowed to contain secrets or to mutate a system of record. Those two constraints are how a non-engineer is allowed to touch code in a customer environment.

An engineer writes the parts that fail in production: connection pooling, timeouts, retries with jitter, idempotency keys, structured logs, metrics, authentication against the user's identity, packaging, CI. If you try to 'quickly add retries' from a blog post, you will build a client that retries POST /create_ticket three times and files three tickets. That is not heroism. That is why the split exists.

Shared ground is the contract between the two of you: the function names that match the design boxes, the JSON schemas, the eval row format, the meaning of empty, the kill switch. You can review those without writing the client. 'Where is the timeout. What does the user see on 429. Where is the prompt version logged. What happens when the folder is empty.' Those questions are the job. Silence in a code review is not courtesy. It is abdication.

Interviews will probe this split. 'Would you implement the RAG pipeline yourself?' The honest answer for most of you: I can write the thin prototype and the eval; I pair with an engineer on the service; I own the spec, the gates, and the go-live. People who claim they will build the platform in the first 90 days are either FDE-shaped with a repo to show, or they are inflating. Do not inflate. The market is hiring the honest version.

If you are the only person in a POC, you still keep the split in the design: v0 is the 40-line script; v1 is the same behavior behind a timeout, a log, and a secret store. Write that down. Otherwise the POC's shortcuts become the production architecture, and you will spend the next quarter explaining why the key is in a notebook.

Interviews will probe this split with a friendly question that is not friendly: 'Would you implement the RAG pipeline yourself?' The honest answer for most of you is a paragraph, not a yes. I can write the thin prototype and the eval. I pair with an engineer on the service. I own the spec, the gates, and the go-live. People who claim they will build the platform in the first 90 days are either FDE-shaped with a repo to show, or they are inflating. Do not inflate. The market is hiring the honest version, and the interviewer has met the other kind. If you are FDE-shaped, say what you have actually run: a 40-line script, a notebook, a retrieve-then-generate file. If you are delivery-shaped, say the review questions you ask and the objects you write. Either way, name the failure modes you will not own: retries that duplicate writes, auth, observability, packaging. Silence on the split is how a hiring loop decides you will either freeze or freelance in production. Neither is the seat.

Shared ground is the contract you can review without writing the client. Function names that match the design boxes. JSON schemas. The eval row format. The meaning of empty. The kill switch. Timeouts. What the user sees on 429. Where the prompt version is logged. Whether the script can mutate anything. Those questions are the job. You can ask them in a PR, in a workshop, and in the fifteen minutes before a call. A delivery lead who sits mute in a code review because 'I am not an engineer' has abdicated the only part of the review they were there to do. The engineer is watching for whether you notice the missing cap and the key in the file. They are not watching for whether you can rewrite their HTTP adapter. On a real squad, write the split on the kickoff slide with names. If a box has no name, the work is unowned. Unowned work ships with both holes: no eval, and no backoff on 429.

Diagram

Labor split on a thin AI slice

A

You write (DM / FDE-light)

  • Eval set and scoring rules
  • Prompt spec and refuse rules
  • Chunking and corpus rules in English
  • Go / no-go, RAID, kill switch
  • Optional: 40-line prototype script
B

Engineer writes

  • HTTP client, retries, timeouts
  • Auth, secret store, identity
  • Observability and redaction
  • Packaging, CI, the actual service
  • Write-path idempotency and audit

Use this in a kickoff. If a box has no name next to it, the work is unowned.

06

A 40-line script is four verbs

Every useful thin-slice script in this industry is some version of load → loop → call → print. Load: find the files, read text, skip junk. Loop: one document (or one eval row) at a time, with a cap. Call: build the messages list, hit the model (or a fake). Print: show the filename and the answer, or write a JSONL row. If you can point at those four verbs in a file you have never seen, you can survive a customer repo in fifteen minutes.

Load is where encodings, paths, and empty files live. The script below reads *.txt from a folder named corpus. That folder name is a constant you would change. Empty files are skipped on purpose so the model is not asked to summarize nothing — which it will happily do, fluently. If your real corpus is PDFs, load becomes 'extract text' and that is an engineering problem (OCR, layout). Do not hide that inside a weekend script. Name it.

Loop is where cost lives. for path in sorted(DOCS.glob('*.txt')) will visit every file. The cap if n >= 5: break is the difference between a demo and a bill. In a review, look for the cap. If there is none, you add one before anyone points this at a 10,000-page share. Sorted() is so the run is repeatable. Random file order makes evals and debugging harder. Repeatability is a delivery feature.

Call is the boundary with money and data. You build messages as a list of dicts: system instruction, then user content (here, a slice of the file so you do not blow the context window on day one). You pass a model name, temperature 0, and the key from the environment. In the sample, call_model is a stand-in that returns the payload as text so you can run the script without spending. In a real client it is HTTP. Keep the boundary as a function so you can swap the stand-in without touching the loop.

Print is the poorest UI and the most honest. You see which file, you see the first bytes of the answer, you see skip reasons. When this becomes a product, print becomes 'write a JSONL trace' and then a screen. Do not start with a screen. Start with a trace you can diff. That is how you debug 'it worked yesterday.'

The change-points you should be able to mark in any such script: the folder (DOCS), the system prompt (SYSTEM), the cap (n >= 5), the slice length (text[:4000]), the model name, the temperature. Those are product decisions hiding as constants. They belong in the design as well as in the file. If they are magic numbers with no comment, you add the comment in the review.

What you ask the author, every time: what happens on an empty file; where the key comes from; what happens if the API errors; whether this can mutate anything; whether the run is capped; whether the output is stored and with what retention. Three of those questions, written in your notes today, are the practice. You are not impersonating an engineer. You are doing a design review on a process that happens to be saved as .py.

The fifteen-minute protocol is load, loop, call, print, then the two gates, then the change-points, then the questions. Load: find the folder constant, the encoding, the skip-empty. Loop: find the cap, confirm sorted or otherwise repeatable. Call: find the messages list, the model name, the temperature, the key source. Print: find what a human would see, and whether a trace is written. Change-points you should be able to mark in any such script: the folder, the system prompt, the cap, the slice length, the model name, the temperature. Those are product decisions hiding as constants. They belong in the design as well as in the file. If they are magic numbers with no comment, you add the comment in the review. You do not need to run the file to do this. You need a margin and a clock. At minute twelve you write three questions. At minute fifteen you join the call and you can point at line numbers. That is the bar. Not LeetCode. Not a tutorial. A process you can repeat on a file you have never seen.

The sample in this concept is rude on purpose: no retries, a stand-in for HTTP, a print instead of a UI, a hardcoded folder. That is legal for a prototype. It is not legal for a service, and it is not legal the moment a secret or a write appears. When a sponsor treats the 40-line file as the product, you name the next version in the same breath: same four verbs, plus timeout, plus a log with a request id, plus the key in a store, plus a cap that cannot be deleted by a well-meaning intern. Write that sentence in the design so the prototype's shortcuts do not become next quarter's architecture. If you can point at the four verbs in a file you have never seen, you can survive a customer repo in fifteen minutes. That is today's whole trick. The worked case that follows is that fifteen minutes, written out, so you can steal the protocol rather than invent it under adrenaline.

Diagram

The 40-line script as a process

01

Load

List *.txt in corpus/. Read UTF-8. This is the folder constant you would change.

02

Gate: empty?

Skip files with no text. Do not pay a model to summarize nothing.

03

Loop with a cap

One file at a time. Stop after N (here, 5). Sorted for repeatability.

04

Call

Build messages[], read API key from env, POST to the model (or a stand-in).

05

Print / trace

Filename + first 200 characters, or a JSONL row. This is your debugger.

If you cannot redraw this from the file, you cannot review it. Four verbs, two gates, one boundary with money.

Annotated thin slice — load, loop, call, print. ~40 lines, no heroics.
import json, os
from pathlib import Path

API_KEY = os.environ["MODEL_API_KEY"]  # crash if missing; never hardcode
DOCS = Path("corpus")                  # change-point: folder
SYSTEM = "Cite sources. Do not invent. If the file is empty, skip it."
CAP = 5                                # change-point: cost cap

def load_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")

def call_model(messages: list) -> str:
    payload = {
        "model": "frontier-small",
        "temperature": 0,
        "max_tokens": 400,
        "messages": messages,
    }
    # Real client: HTTP POST with Authorization: Bearer API_KEY
    # Stand-in so you can read the script without spending:
    return json.dumps(payload)

def main() -> None:
    n = 0
    for path in sorted(DOCS.glob("*.txt")):
        if n >= CAP:
            break
        text = load_text(path)
        if not text.strip():
            print(f"skip empty: {path.name}")
            continue
        messages = [
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": text[:4000]},
        ]
        answer = call_model(messages)
        print(path.name, answer[:200])
        n += 1

if __name__ == "__main__":
    main()

Worked case · stay here ~20 minutes

Fifteen minutes in the customer repo

Tuesday, 9:44 a.m. Atlas POC at Northwind. Workshop with Marcus, the sponsor, starts at 10:00. Priya, the engineer, dropped a Slack link: scripts/summarize.py, about forty lines, we will walk it. You have not seen the file. You have a laptop that is allowed on their network and a timer.

The Slack message lands at 9:44. Workshop is at 10:00. Priya, the engineer on the Atlas POC, has pasted a GitHub link and one line: scripts/summarize.py, about forty lines, we will walk it with Marcus. You have not opened the repo. You clone it onto the laptop that is allowed on their network, you do not paste the URL into a consumer chat, and you do not ask Priya to screenshare yet. You want your own eyes on the file before you are performing. Finder view: a corpus folder with a handful of .txt files, a requirements.txt, a .env.example, no README that would save you. summarize.py is forty-three lines. That is the object. The job for the next twelve minutes is not to become a Python person. It is to be able to point at load, loop, call, and print, and to know whether a key is in the file, whether empty files get sent to a model, and whether the run is capped. Put a timer on. Fifteen minutes is not a vibe. It is the time you have.

Open the file from the top. Imports: json, os, pathlib. Those are boring and that is good. Then a line that reads os.environ['MODEL_API_KEY']. Crash if missing. You look for a string that starts with sk- or a Bearer token assigned in the file. There is none in summarize.py. You still grep the tree before you relax: notebooks, a config.py, a screenshot in /docs. Two minutes. The next lines are constants: DOCS = Path('corpus'), a SYSTEM string that says cite sources and do not invent, CAP = 5. Those three names are the product hiding in the file. Folder, contract, budget. You write them in the margin as change-points. You do not need to know pathlib to see that corpus is the folder you would change if Marcus says the SOPs live in sharepoint-export/. You do not need to know os.environ to see that the key is not supposed to be in git. First scan done. You have not read the functions yet and you already have something to say on the call if the rest of the file is messy.

def load_text(path) returns path.read_text(encoding='utf-8'). That is the load verb. Encoding is explicit, which is adult. If a Northwind SOP came from a Windows export you may need utf-8-sig later, and you note that as a question rather than a rewrite. The function does not catch missing files, does not skip binaries, does not OCR. For a folder of .txt that is correct. If Marcus's real corpus is PDFs, load becomes extract-text and that is an engineering workstream, not a weekend patch. You write that sentence down so you can say it when someone asks 'can we just point it at the PDF drive after lunch.' You also note what load does not do: it does not attach metadata, it does not drop cookie banners, it does not check ACL. Those absences are legal in a forty-line prototype and fatal in a service. The split from today's concepts is already in the file, whether Priya named it or not.

def main is the loop. for path in sorted(DOCS.glob('*.txt')). Sorted is repeatability. Random file order makes debugging a he-said-she-said. Inside the loop: if n >= CAP: break. Five files. That line is the difference between a demo and a bill. You look for it the way you look for a budget on a statement of work. If it were missing you would add it before anyone pointed this at a ten-thousand-page share, and you would say so in the first two minutes of the workshop rather than after the invoice. n starts at zero and increments only after a successful call, which means skipped empty files do not eat the cap. That is a product choice. You note it. You do not yet know whether Priya meant it. The body of the loop is 'do the same process to the next file.' You have run batches. The new part is that the process costs money and can leak a key. The cap is a delivery control that happens to be written in Python. Treat it that way on the call, not as syntax trivia.

Inside the loop, text = load_text(path), then if not text.strip(): print skip empty and continue. That is the gate. The model is not asked to summarize nothing, which it would happily do, fluently, and Marcus would screenshot the fluent nothing as a failure of 'AI.' The gate is the sentence 'if we do not have sources, we do not call.' You already love gates. Here is one in a file. You also note what the gate does not do. It does not refuse a file that is only a cookie banner. It does not refuse a file that is three lines of nav chrome. It does not look at ACL. For corpus/ with four real SOPs and one empty, it is enough. For Confluence export, it is not. You are collecting the v0 versus v1 list while you read, which is the only way the prototype's shortcuts stay named. At minute six you have load, loop, cap, empty-gate. You have not yet spent a token. That is the right order.

The call is the boundary with money and data. messages is a list of two dicts: system with SYSTEM, user with text[:4000]. The slice is a context-window control hiding as a subscript. Four thousand characters is not four thousand tokens, and you do not pretend it is, but you see the intent: do not blow the window on day one. call_model builds a payload dict with model frontier-small, temperature 0, max_tokens 400, and the messages. Temperature 0 is the production default you wanted. Model name is a constant you would pin. In this file call_model does not POST. It returns json.dumps(payload) so you can run the script without spending. The comment says so. That stand-in is honest. A file that claimed to call the model and actually printed the payload is a prototype. A file that called the model with a key in the source is an incident. You prefer the stand-in. You will still ask where the real client lives and whether it has a timeout.

You now grep. MODEL_API_KEY in summarize.py is only the environ read. .env.example has MODEL_API_KEY= with an empty value and a comment, do not commit the real key. .gitignore lists .env. No notebooks in the tree. No key in the commit log that git log -S sk- would find in the two minutes you have. You still do not run the script against Northwind's real SOPs on this laptop without Priya confirming the key is a dedicated POC key with a spend cap. Borrowing someone's production key to 'just try before the meeting' is how you look dangerous. You write the personal rule on the same page as the change-points: keys in the environment, crash if missing, rotate if it leaked, I do not paste this key into Slack or into a screenshot of a working terminal. If Marcus asks whether the demo is safe, the honest answer is: the file is safe to read; running it is safe only with a scoped key and the cap left on. That is a better sentence than 'yes' or 'I am not an engineer so I cannot say.'

Print is the poorest UI and the most honest. print(path.name, answer[:200]) shows which file and the first bytes of the answer, or of the dumped payload if you are on the stand-in. Skip reasons are printed too. When this becomes a product, print becomes a JSONL trace with a request id, then a screen. Starting with a screen is how you debug from screenshots. Starting with a trace you can diff is how you debug 'it worked yesterday.' You would rather Marcus see a terminal for this workshop than a chatbot skin that hides the filename. If Priya has already built a Streamlit box, you still want the print. Ask her to run with the cap at 2 so the room watches two files, one empty skip, one summary, and then stops. That run is the demo. A run that silently walks the whole folder while people talk is a bill and a chance to leak. The print line is also where you will see None blow up if load_text ever returns nothing other than a string. Tracebacks are data. Do not restart the laptop. Read the line.

requirements.txt lists three packages with versions. There is a venv in the README Priya did not write, but .env.example mentions one. You will not install into system Python 'just this once' on a customer laptop. If you cannot run it this morning, you can still finish the workshop: you have traced the file. FDE-shaped you may activate the venv and run against corpus/ with the stand-in, no key required, to show Marcus the skip and the print. Delivery-shaped you may not. Either way you do not fake a local setup you do not have. You also do not paste the script into a consumer model to 'explain it,' because the script is customer code even if it is forty lines of prototype. The bill of materials, the venv, the working directory — those are the stack from today's diagram, sitting in a real folder. If any layer is missing, the demo is a snowflake. Snowflakes do not survive the second engineer. Say that if Marcus wants to 'just email the script around.'

Change-points you can now mark for Marcus without a tutorial. DOCS is the folder. SYSTEM is the contract. CAP is the budget. text[:4000] is the stuffing limit. model and temperature sit in the payload. Those are product decisions. If they stay as magic numbers with no comment, you add the comment in the review after the workshop, not during it. During it, you say: we can point this at a different folder, we can change the instruction, we can stop after five, we will not raise temperature to make it sound friendlier, we will not remove the cap to be impressive. Marcus will ask whether it can do the whole share. The answer is: not this file, and not this morning. v0 is forty lines and a cap. v1 is the same four verbs with a timeout, a log, a secret store, and an eval. You can say that sentence because you read the file. You could not have said it from Priya's Slack one-liner. That is why you used the fifteen minutes on the repo instead of on the slide.

Three questions for Priya, written before you join, because you will not remember them once Marcus is talking. One: what happens if the API returns 429 or empty content — exception, retry, or a printed skip? The stand-in never fails, so the real client is the risk. Two: can this mutate anything, write back to disk, post a ticket, send mail? You see only print, but you have not seen the branch she will demo. Three: where is output stored, with what retention, and does a copy of the SOP leave the tenant when the real client is wired? You will not ask all of these as a quiz. You will ask the one that matches whatever Marcus claims. If he says we will hook it to Confluence after lunch, you ask the mutate and the secret questions. The list is a design review on a process that happens to be saved as .py. That is the job. The syntax was never the job.

At 9:59 you join. You do not lead with 'I read Python.' You lead with the four verbs and the controls. 'The script loads text from corpus/, skips empty, stops after five, builds a messages list with a cite-and-do-not-invent instruction, and prints the filename and the first two hundred characters. The key is not in the file. The model call is still a stand-in. What we would change to point at your SOP export is the folder constant. What we will not do this morning is uncap it or paste a key into chat.' Marcus can hear that. Priya can correct the one thing you misread. You look like the person who opened the repo, which is the bar for FDE and for any delivery lead who claims they can sit in the room. If you had waited for the screenshare, you would now be nodding at a scroll. Nodding is not a review. Line numbers are. You have three questions left in your notes for the moment the demo goes quiet. That is a good place to be when the clock starts.

Diagram

Fifteen-minute repo protocol

01

Open + grep keys

Clone on an allowed laptop. Crash-if-missing environ. Grep sk-, Bearer, notebooks. .env gitignored.

02

Mark the four verbs

Load, loop, call, print. Names should match the design. If they do not, that is the first sentence on the call.

03

Gates and cap

Empty skip. n >= CAP. Sorted for repeatability. Uncapped is a blank cheque.

04

Change-points

Folder, system prompt, cap, slice, model, temperature. Product decisions hiding as constants.

05

Three questions

API errors, mutate anything, where output lives. Join the call with line numbers, not with a nod.

Steal this for the next customer file. If a step is missing, you are performing, not reviewing.

Practice

Trace and annotate

50 minutes

Pretend this script is in a customer repo and you have 15 minutes before a call.

  1. Copy the sample below into notes. Number the steps in the margin: load, loop, call, print.
  2. Write what you would change to (a) point at a different folder, (b) change the system prompt, (c) stop after 5 files.
  3. Write three questions you would ask the author (error handling, secrets, what happens on empty files).
  4. Optional if you have Python: type a 10-line script that reads a text file and prints the first 200 characters. No API needed.

Done looks like: An annotated script and three decent review questions. Optional: a 10-line local run.

Check yourself

Attempt in your notes first. Reveal is for after, not during.

  • What Python shape is a chat message?

  • Where do API keys go?

  • What is 'just enough Python' for this pivot?

  • Name the four verbs of a thin-slice script.

  • What does json.JSONDecodeError usually mean on a file you were handed?

  • What do you write vs what an engineer writes on this slice?

Terms from this day

dict
A key→value map. JSON objects become dicts in Python. The default shape of AI payloads.
JSONL
A file with one JSON object per line. The usual format for eval sets and logs.
Environment variable
A named value in the process environment. Standard place for secrets.
requirements.txt
A list of Python packages a project needs. The bill of materials.
Virtualenv
An isolated Python package environment per project.

If you have extra minutes

  • Python tutorial — official docs, sections on lists, dicts, files

    Only if the syntax still feels alien. Do not start a full Python course this month.

Your notes for day 8

Saved on this device. Use this as the start of the artifact.