summarize_channel returns a tuple of spend and CPL in one shot — clean. Now picture where that data comes from: a Salesforce or HubSpot export sitting on your desktop as a .csv file. How do you usually open it?
Excel, always. Double-click, it opens. I've never thought about what's actually inside the file before Excel gets to it.
It's just text. Every row is a line, every column is separated by a comma. Python's open() and with block are the standard way to read that text — you'd write with open("campaigns.csv") as f: text = f.read() and you'd have the whole file as a string. But the Pyodide sandbox has no files on disk, so we do something equally useful: the caller reads the file and passes the text in. The function's job is parse, not read:
with open("campaigns.csv") as f:
text = f.read() # standard pattern outside sandbox
# in our sandbox: the text arrives as an argument instead
def parse_campaign_csv(csv_text: str) -> list:
...So the function signature looks the same as any other function — it just receives text instead of a filename? The caller handles the file part?
Exactly. And that separation is actually better design regardless of the sandbox — pure functions that transform data are easier to test than functions that also do I/O. Now, once you have the text, splitting it is two lines: text.strip().split("\n") gives you a list of row strings, and row.split(",") gives you a list of column values per row. The header is row zero — skip it with rows[1:]. Spend is a float, leads is an int, and the name goes through clean_campaign_name:
def parse_campaign_csv(csv_text: str) -> list:
rows = csv_text.strip().split("\n")
result = []
for row in rows[1:]: # skip header
parts = row.split(",")
name = clean_campaign_name(parts[0])
channel = parts[1]
spend = float(parts[2])
leads = int(parts[3])
result.append({"name": name, "channel": channel, "spend": spend, "leads": leads})
print(f"Parsed {len(result)} campaigns")
return resultclean_campaign_name is already doing the strip and lower — so even if the export has " EMAIL BLAST " in that first column, it comes out clean before it hits the dict. That's the compose payoff.
You just described middleware. Welcome to the pipeline mindset.
One thing I'd mess up — if I forgot rows[1:] and included the header, float("spend") would blow up. Is there a nice way to guard that?
The simplest guard is a clear contract: your function's docstring says "csv_text must include a header row" and you skip it unconditionally. If the caller passes headerless text, that's their bug, not yours. Day 27 is where we handle messy-data defensively with try/except — for now, trust the contract and keep the function readable.
The standard Python file-reading pattern:
with open("campaigns.csv") as f:
text = f.read()with closes the file automatically. In the Pyodide sandbox there are no files, so the caller supplies the text as an argument — the parsing logic is identical either way.
| Step | Code | Result |
|---|---|---|
| Strip + split lines | text.strip().split("\n") | list of row strings |
| Skip header | rows[1:] | data rows only |
| Split columns | row.split(",") | column values |
| Convert types | float(parts[2]), int(parts[3]) | numeric fields |
float("spend") raises ValueError..strip() — trailing whitespace corrupts column values.summarize_channel returns a tuple of spend and CPL in one shot — clean. Now picture where that data comes from: a Salesforce or HubSpot export sitting on your desktop as a .csv file. How do you usually open it?
Excel, always. Double-click, it opens. I've never thought about what's actually inside the file before Excel gets to it.
It's just text. Every row is a line, every column is separated by a comma. Python's open() and with block are the standard way to read that text — you'd write with open("campaigns.csv") as f: text = f.read() and you'd have the whole file as a string. But the Pyodide sandbox has no files on disk, so we do something equally useful: the caller reads the file and passes the text in. The function's job is parse, not read:
with open("campaigns.csv") as f:
text = f.read() # standard pattern outside sandbox
# in our sandbox: the text arrives as an argument instead
def parse_campaign_csv(csv_text: str) -> list:
...So the function signature looks the same as any other function — it just receives text instead of a filename? The caller handles the file part?
Exactly. And that separation is actually better design regardless of the sandbox — pure functions that transform data are easier to test than functions that also do I/O. Now, once you have the text, splitting it is two lines: text.strip().split("\n") gives you a list of row strings, and row.split(",") gives you a list of column values per row. The header is row zero — skip it with rows[1:]. Spend is a float, leads is an int, and the name goes through clean_campaign_name:
def parse_campaign_csv(csv_text: str) -> list:
rows = csv_text.strip().split("\n")
result = []
for row in rows[1:]: # skip header
parts = row.split(",")
name = clean_campaign_name(parts[0])
channel = parts[1]
spend = float(parts[2])
leads = int(parts[3])
result.append({"name": name, "channel": channel, "spend": spend, "leads": leads})
print(f"Parsed {len(result)} campaigns")
return resultclean_campaign_name is already doing the strip and lower — so even if the export has " EMAIL BLAST " in that first column, it comes out clean before it hits the dict. That's the compose payoff.
You just described middleware. Welcome to the pipeline mindset.
One thing I'd mess up — if I forgot rows[1:] and included the header, float("spend") would blow up. Is there a nice way to guard that?
The simplest guard is a clear contract: your function's docstring says "csv_text must include a header row" and you skip it unconditionally. If the caller passes headerless text, that's their bug, not yours. Day 27 is where we handle messy-data defensively with try/except — for now, trust the contract and keep the function readable.
The standard Python file-reading pattern:
with open("campaigns.csv") as f:
text = f.read()with closes the file automatically. In the Pyodide sandbox there are no files, so the caller supplies the text as an argument — the parsing logic is identical either way.
| Step | Code | Result |
|---|---|---|
| Strip + split lines | text.strip().split("\n") | list of row strings |
| Skip header | rows[1:] | data rows only |
| Split columns | row.split(",") | column values |
| Convert types | float(parts[2]), int(parts[3]) | numeric fields |
float("spend") raises ValueError..strip() — trailing whitespace corrupts column values.Morgan's team exports campaign data from HubSpot as a CSV string with columns `name`, `channel`, `spend`, and `leads`. The names come out inconsistently capitalised and may carry underscores. Write `parse_campaign_csv(csv_text)` that skips the header row, calls `clean_campaign_name` on the name column, converts `spend` to `float` and `leads` to `int`, and returns a list of dicts — one per row. Assume `clean_campaign_name` is already defined.
Tap each step for scaffolded hints.
No blank-editor panic.