Skip to content

← Under the Hood: Objects, Memory, Speed step 23 of 35

Medium Primitives

render_report: join for str, bytearray for bytes

Render a CSV report twice — once as str, once as bytes — using the accumulation strategy that is linear unconditionally rather than the one that is linear until someone adds a log line.

def render_report(rows: Iterable[Row]) -> str: ...
def render_bytes(rows: Iterable[Row]) -> bytes: ...

Both emit the header "name,qty,price\n" followed by one line per row, formatted f"{name},{qty},{price:.2f}\n". render_bytes must produce exactly render_report(rows).encode("utf-8").

render_report. The starter uses text += ... in a loop. On CPython that is currently fast, because BINARY_OP_INPLACE_ADD_UNICODE mutates the string in place when the next opcode is STORE_FAST and the local holds the only reference. Both conditions are outside your control: an alias (t = text for a debug line), an attribute target (self.buffer += ...), or a different interpreter all disable it, and the measured penalty for the alias case is 105x. Accumulate into a list[str] and "".join once.

render_bytes. The starter has two bugs. bytes is immutable and gets no in-place special case at all, so blob += ... reallocates and copies every iteration — genuinely quadratic. And it encodes as "ascii", which is a guess about the data rather than a fact about it; one of the test rows is named café. Use a bytearray (the FAQ’s own recommendation for bytes accumulation) and encode as UTF-8.

solve(raw, repeat) parses raw — rows of [name, qty, price] as strings — repeat times over, renders both forms, and returns:

{"length": int, "head": str, "tail": str, "lines": int,
 "bytes_match": bool, "digest": str}

where head/tail are the first and last 40 characters and digest is the first 16 hex characters of sha256 over the bytes rendering.

One hidden case renders 200,000 rows. Nothing asserts a duration — the harness cannot time reliably — but a genuinely quadratic accumulation (out = out + [line], or bytes +=) does not return inside the limit, so the scale is the test. The reasoning is the deliverable here: you should be able to say which of your accumulations depends on a CPython implementation detail and which does not.

Your submission must pass mypy --strict.