Why here, and not first. You already write Python, so a track that teaches it is wasted. Its job in this series is to be the contrast: read immediately after Java 8, the same program in a third of the ceremony, and the question stops being "which is better" and becomes "what exactly did the ceremony buy, and when is that worth it". That question is answerable only by having written both.
It is also the reference implementation —
bench/expected/logagg.txtis generated from this file — which makes it the one track where being unambiguous matters more than being clever.
Guido van Rossum, CWI Amsterdam, 1991, as a successor to a teaching language called ABC. The
founding constraint was readability by people who are not primarily programmers, and it is
visible in every decision: significant whitespace, for x in xs, no braces, one obvious way
to do it.
Python spent its first fifteen years as a scripting and glue language and then won two enormous territories almost by accident: scientific computing (NumPy, 2006 — because the C extension API made it a comfortable front end for FORTRAN and C numerical code) and, on top of that, machine learning. Neither was designed for. Both happened because Python is unusually good at being a driver for fast code written in something else.
Who pays for it today: ML and data science (near-totally), data engineering, DevOps and infrastructure automation, scientific research, web backends (Django, FastAPI), and an uncountable number of internal scripts that quietly became load-bearing.
There is no build.
python logagg.py bench/data/access.log
What replaces the build step is environment management, and it is the single hardest practical thing about Python — harder than the language:
| Tool | What it is |
|---|---|
venv |
Stdlib, creates an isolated interpreter + site-packages. The baseline. |
pip |
The installer. Resolves poorly, but universal. |
poetry / pdm |
Lockfiles and reproducible resolution |
uv |
Rust-implemented, dramatically faster, now the default recommendation |
conda |
Separate ecosystem; handles non-Python native deps, which matters for scientific stacks |
Shipping is where Python is genuinely weak. There is no equivalent of a static binary or a
fat jar. The options — a container, pyinstaller, zipapp, or "just install Python on the
target" — are all worse than what C and Java offer, and this is the main reason Go took so
much of Python's infrastructure-tooling territory after 2012.
A variable is a name bound to an object in a namespace. Not a memory cell (C), not a
reference slot with a declared type (Java) — a dictionary entry. x = 5 puts the name x
into a namespace dict pointing at an int object. Rebinding x changes the dict; it does not
change any memory the old value occupied.
Follows directly:
a = b never copies. Two names, one object.int costs ~28 bytes.Typing is dynamic and strong. Dynamic: types live on values, not names. Strong: 1 + "x"
is a TypeError, not "1x" — which is the distinction JavaScript fails and the reason
"dynamically typed" alone is not a useful description of a language.
Type annotations (3.5+) are real and useful and not enforced at runtime. mypy or
pyright check them; the interpreter ignores them entirely. logagg.py is fully annotated
for exactly that reason — the annotations are documentation the tooling happens to verify.
Memory is reference counting, plus a cycle collector. That combination is unusual and has consequences:
del f closes the file. C++ programmers find this familiar and Java programmers do not.The module, and then the object — but the object is a bag of attributes.
Python has classes, but it does not believe in them the way Java does. There is no private
(only a _name convention and __name mangling), attributes can be added at runtime, and any
object with the right method satisfies any "interface":
stats = res.services.setdefault(service, Stats())
Stats is a @dataclass here because it is a bag of four counters with a method, and
@dataclass generates __init__, __repr__, and __eq__ from the annotations. The Java
track needed the same thing and wrote the constructor by hand — and Java 16's record is
precisely this feature, arriving eight years later.
Duck typing is the real abstraction mechanism. logagg's aggregate() takes a path, but
a version taking any iterable of strings would work identically for a file, a list, a socket,
or a generator — with no interface declared anywhere. That is enormously convenient and it is
also why Python codebases past ~50k lines start adding Protocol types and a type checker:
the contract exists, it was just never written down.
The module is the more important unit in practice. import is the namespacing mechanism, a
module is a singleton object, and "just put it in a module-level function" is idiomatic in a
way it simply cannot be in Java.
Exceptions, all unchecked, and used for control flow far more liberally than in Java.
The cultural rule is EAFP — easier to ask forgiveness than permission:
try:
value = d[key]
except KeyError:
value = default
is idiomatic, where the Java instinct (if (d.containsKey(key))) is described disapprovingly
as LBYL. Iteration itself terminates by raising StopIteration. Exceptions are cheap and
ordinary here in a way they are not elsewhere.
logagg.py deliberately does not use them for the malformed-line path:
reason = classify(fields)
if reason is not None:
res.malformed.append((lineno, reason))
continue
The EAFP version — try: micros = parse(...) except ValueError: — is arguably more Pythonic
and would be wrong for this spec, because SPEC 1 requires a specific ordered reason, and
catching ValueError cannot tell you whether it was the status or the latency that failed.
That is a small, real instance of a general truth: idiomatic and correct are not the same
axis, and the spec wins.
The fatal path catches OSError — the base class covering FileNotFoundError,
PermissionError, and IsADirectoryError, all of which must produce RC 12.
Threads that cannot run Python bytecode in parallel.
The Global Interpreter Lock is one mutex around the interpreter. Only one thread executes bytecode at a time. It exists because reference counts would otherwise need atomic updates on every single object touch, which would cost more in single-threaded performance than multi-core parallelism gains for most Python programs. It is a defensible trade that has aged badly.
What that means concretely:
| Workload | Use | Why |
|---|---|---|
| CPU-bound | multiprocessing |
Separate interpreters, real parallelism, pay for IPC |
| I/O-bound | threading or asyncio |
The GIL is released during I/O waits |
| Numeric | NumPy, etc. | The GIL is released inside the C extension |
For --shard N (SPEC 6), the Python answer is multiprocessing.Pool, each worker returning a
partial dict, merged in the parent. The threading version would be slower than serial —
the GIL serialises the work and you pay for the context switching. Writing both and timing
them is the exercise, and the result is the most durable thing this track teaches.
Worth knowing for the follow-up: PEP 703 (free-threaded CPython, optional-no-GIL) shipped as an experimental build in 3.13 and is on a multi-year path to becoming the default. This section will need rewriting, and that is a good reason for it to be in git.
logagglogagg.py · annotated walkthrough in NOTES.md
PASS python
~190 lines, and most of the difference from Java is in three places:
| C | Java 8 | Python | |
|---|---|---|---|
| Growable line buffer | 45 lines | in.readLine() |
for raw in fh |
| Hash map | 60 lines | computeIfAbsent |
setdefault |
| Sort by two keys | qsort + comparator |
Comparator.comparingLong(...).reversed().thenComparing(...) |
key=lambda kv: (-kv[1].count, kv[0]) |
That last row is the most telling. (-count, name) is a tuple, compared element-wise, with
the negation doing the descending sort. It is shorter than the Java comparator chain and, more
to the point, it has no type-inference ambiguity to work around — the cost of Java's version
was not verbosity but the (Stats s) annotation the compiler needed.
Two places Python's defaults had to be overridden:
with open(path, "r", encoding="ascii", newline="") as fh:
newline="" disables universal-newline translation. Without it a CRLF fixture is silently
normalised, which sounds helpful and means the program cannot detect a real problem in its
input. encoding="ascii" is explicit for the same reason Locale.ROOT was in Java: the
default is read from the environment.
sys.stdout.buffer.write(render(source, res).encode("ascii"))
print() on Windows emits \r\n. Same problem as C's _setmode and Java's System.out.write
— all three tracks hit it, and all three solve it differently, which is a better
illustration of what a "standard library" is than any amount of prose.
def f(xs=[]) evaluates [] once, at definition. Every call
shares the same list. The fix is xs=None.[lambda: i for i in range(3)] gives three functions all
returning 2. i is looked up when called, not when created.is versus ==. is compares identity. It appears to work for small integers and
short strings because CPython interns them, then fails for 1000. Use it only for None.list(xs) copies the list, not the objects in it. copy.deepcopy exists
and is usually a sign the data model is wrong.except: catches KeyboardInterrupt and
SystemExit. Always except Exception: at minimum, and name the exception you expect.mypy in anything long-lived.Recommended: CPython's Lib/dataclasses.py. You use @dataclass in this very track, it
is pure Python, and it is a masterclass in metaprogramming — it literally builds __init__ as
a source string and execs it, with a comment explaining why that is faster than the
alternatives. Reading it turns the decorator from magic into machinery.
Alternative: Lib/collections/__init__.py — OrderedDict and Counter. Short, familiar,
and full of the small defensive habits of stdlib code.
For the other direction: httpx or attrs — modern, well-typed third-party code showing
what Python looks like when annotations are taken seriously from the start.
with blocks and __del__ that fire predictably. When Rust's
Drop arrives later in the series it will look familiar rather than novel.classify() is better
Python and fails the spec. Knowing when to override the idiom is a senior skill, and this is
the smallest clear example of it in the series.