Skip to content

← Memory, Copies and Slow Code step 24 of 38

Medium Primitives

Build a Report as Text and UTF-8 Bytes

Your shop exports its stock as a small comma-separated report. Write the two functions that render it, one as text and one as UTF-8 bytes. The starter builds both with += in a loop. The bytes version copies the whole report for every line, so it slows down badly on large reports, and its ASCII encoding fails on a name like café. The text version is fast only because of the CPython shortcut that the article describes.

rows = [Row("widget", 3, 1.5), Row("café", 1, 9.999)]
assert render_report(rows) == "name,qty,price\nwidget,3,1.50\ncafé,1,10.00\n"
assert render_bytes(rows) == render_report(rows).encode("utf-8")

What to write

  • render_report(rows: Iterable[Row]) -> str appends the lines to a list[str] and joins them once.
  • render_bytes(rows: Iterable[Row]) -> bytes adds each UTF-8 encoded line to a bytearray, then converts it to bytes once at the end.

Both use the same format:

  • Start with HEADER, which is "name,qty,price\n", even when there are no rows.
  • Then write one line per row, in input order, formatted exactly as f"{row.name},{row.qty},{row.price:.2f}\n". Every line ends in a newline.
  • Write names as they are, with no quoting or escaping. This is a simple comma-separated format, not a full CSV writer.
  • Format prices with .2f, so 9.999 becomes 10.00. Don't round them any other way. Python stores a float such as 12.005 as a value slightly off from what you typed, and .2f rounds the stored value.
  • For the same rows, render_bytes returns exactly what render_report(rows).encode("utf-8") returns.

Each function must work on input that can be read only once, such as a generator. Loop over rows once, and give each renderer its own input.

What the driver reports

Keep Row, HEADER, parse and solve as they are. parse(raw, repeat) turns each [name, qty, price] row of strings into a Row, and repeats the whole list repeat times. A repeat of zero or less gives no rows, and the header still appears once. solve renders the same rows both ways and returns:

Key Meaning
length len(text), which counts characters, not bytes
head the first 40 characters of the text, or all of it if shorter
tail the last 40 characters of the text, or all of it if shorter
lines the number of newlines in the text, including the header's
bytes_match whether the bytes equal text.encode("utf-8")
digest the first 16 hex characters of the SHA-256 of the bytes

The largest test renders 200,000 rows. Your submission must pass mypy --strict.