curayious Language Lab

Python — lab notes

This track is the reference implementation: bench/expected/logagg.txt is generated from it and frozen. So the bar is different from the other tracks — it has to be obviously correct on re-read, not clever. When the Rust version disagrees in eight months, this is the file that gets read to settle it.


Why the golden file is frozen

python tracks/30-python/logagg.py bench/data/access.log > bench/expected/logagg.txt

Run once. After that it is a checked-in artefact and changing it requires changing bench/SPEC.md, reviewed in the same diff. Regenerating it casually to make a failing track pass defeats the entire apparatus — the golden file's value is exactly that it does not move.

The same discipline as the COBOL lab's differential testing: the legacy output is the specification, and a "better" answer is still a wrong answer.

Two defaults that had to be overridden

newline=""

with open(path, "r", encoding="ascii", newline="") as fh:

Default Python does universal-newline translation: \r\n and \r both arrive as \n. That sounds helpful and means the program cannot detect a CRLF file. With newline="" the \r survives, gets attached to the latency field, and is reported as a LATENCY rejection — which is a true statement about the input rather than a silent repair of it.

encoding="ascii" is explicit for the same reason Locale.ROOT was needed in Java: without it the default encoding is locale.getpreferredencoding(), which on Windows is still a legacy code page (cp1252 here) unless UTF-8 mode is enabled. PEP 686 will make UTF-8 the default, but it is not landed, and code that has to work on today's interpreters names its encoding.

sys.stdout.buffer.write

sys.stdout.buffer.write(render(source, res).encode("ascii"))

print() on Windows emits \r\n. Third track, third instance of the same problem, third different fix:

Track Fix
C _setmode(_fileno(stdout), _O_BINARY)
Java 8 System.out.write(s.getBytes(US_ASCII))
Python sys.stdout.buffer.write(s.encode("ascii"))

Worth keeping that table. It is a cleaner illustration of what "standard library" means than any amount of prose about batteries being included.

Where idiomatic and correct diverge

The EAFP version of the validator is more Pythonic:

try:
    micros = parse_micros(fields[5])
    status = int(fields[4])
except (ValueError, IndexError):
    res.malformed.append((lineno, "???"))

and it is wrong for this spec, because SPEC 1 requires a specific ordered reason and a caught ValueError cannot tell you whether the status or the latency failed. So classify() checks explicitly, in order, LBYL-style, and returns the reason:

if len(fields) != 6:
    return "FIELDS"
...

Smallest clear example in the series of a general truth: idiomatic and correct are different axes, and when they disagree the spec wins. Worth flagging in the Q10 of every track where it comes up.

[0-9] and not \d

LATENCY_RE = re.compile(r"^[0-9]+\.[0-9]{3}$")

In Python 3, \d on a str pattern matches any Unicode decimal digit — Arabic-Indic ٠١٢, Devanagari, fullwidth 012. SPEC 1 says ASCII digits. re.ASCII would fix it; so does spelling out the class, which has the advantage of meaning the same thing in the Java track (where \d is ASCII-only by default) and in every other track after it.

One regex, one meaning, everywhere. Cheap insurance.

The dead helper

First draft had a half-finished row() function left above render() — a placeholder with a .rjust(0) in it that did nothing. It never ran, because render() defines its own nested line(). Deleted.

Worth noting only because it compiled, imported, and passed every test. In the C and Java tracks an unfinished function would have been a compile error. This is the cost of dynamic typing in its smallest form: errors wait on the branch that is not taken. A linter catches this one; the general case does not have a linter.

Things not done yet