How does Python's `:=` turn a chunked read-until-EOF loop into one condition?
answer
- It removes the priming read
- No more `while True` with a break
- The exit test moves into the header
- Truthiness is not the same as exhausted
basics
~10 swhile chunk := src.read(8192): performs the read, binds the result and tests it in one place, replacing the while True / read / break shape or the duplicated priming read that Python otherwise forces.
solid answer
~50 sBefore assignment expressions a read-until-exhausted loop needed either a priming read plus a duplicate read at the bottom, or `while True:` with an `if not chunk: break` buried in the middle. Both split the read from the test. `while chunk := src.read(8192):` puts the call, the binding and the termination test in a single line, and the loop ends when the read returns an empty result. The critical caveat is that the condition is a *truthiness* test, not an end-of-stream test: it is correct for `read`, whose only falsy return is the empty `bytes` or `str` at EOF, but wrong for any source that can legitimately yield `0` or an empty string mid-stream — there you must compare against an explicit sentinel, as in `while (line := src.readline()) != "":`. The other classic trap is mixing types: a binary read returns `bytes`, so comparing it against a `str` sentinel never matches.
code
python · 12 linesimport hashlib
import io
src = io.BytesIO(b"\xff\xd8" + b"x" * 20_000)
digest = hashlib.sha256()
chunks = 0
while chunk := src.read(8192):
digest.update(chunk)
chunks += 1
print(chunks, digest.hexdigest()[:12])go deeper
Recognise and be able to write the idiom: while chunk := src.read(8192): reads, binds and tests in one line, and the loop ends when the read comes back empty.
Explain what it replaces — a duplicated priming read or a while True with a hidden break — and that the condition is a truthiness test, so a legitimately falsy value would end the loop early.
Demonstrate the operational judgement: choose the chunked form to bound memory in a long-lived worker, use an explicit sentinel when falsy values are meaningful, and keep the sentinel's type matched to the read mode.
Frame it as a correctness-of-idiom question for the codebase: decide when truthiness conditions are acceptable versus explicit sentinels, since the failure mode is silent truncation rather than an exception.
### The shape the operator was designed for Consider an image-thumbnail worker that streams each uploaded source file through a hash and a resizer rather than loading it whole — the process has a 45-second cold start, so it is long-lived and handles many files, and holding an entire multi-megabyte upload in memory per request is exactly the kind of spike that makes it fall over. Streaming means a read loop. Python's `read(size)` returns *up to* `size` bytes and returns an empty result once the source is exhausted, so the loop has to read, test, and use the same value. Without `:=`, that forces one of two awkward shapes: ```python chunk = src.read(8192) # priming read while chunk: digest.update(chunk) chunk = src.read(8192) # duplicated # or while True: chunk = src.read(8192) if not chunk: break digest.update(chunk) ``` The first duplicates the read — and a maintainer who changes the chunk size in one place and not the other has introduced a real bug. The second hides the termination condition three lines into the body, so `while True` tells the reader nothing. The assignment expression collapses both: ```python while chunk := src.read(8192): digest.update(chunk) ``` Read, bind, test — one line, one occurrence of the call, and the loop header states the actual exit condition. This is the example PEP 572 itself leads with, and it is the use nearly every style guide accepts without argument. ### Truthiness is not end-of-stream The condition tests whether the bound object is truthy, and that is only *accidentally* the same as "not exhausted". It holds for `read` on a file object, because the sole falsy value it can return is the empty `bytes` or `str` that marks EOF. It does **not** hold in general: * a work item that is legitimately `0` or `""` ends the loop early; * `readline` is safe for EOF (a blank line comes back as `"\n"`, which is truthy) but a stream whose last line lacks a newline still behaves, so people over-generalise from it; * any source that can yield an empty-but-meaningful value needs an explicit test. The disciplined form is to compare against the sentinel you actually mean: ```python while (line := src.readline()) != "": handle(line) ``` Now the intent is explicit and a falsy-but-valid value cannot terminate the loop. ### The type mismatch that eats an afternoon The other failure in this idiom is an encoding mismatch. Open the source in binary and `read` returns `bytes`; open it in text mode and it returns `str`, decoded with a codec that depends on the platform default unless you pass `encoding=`. Feed image bytes through a text-mode read and you get a `UnicodeDecodeError` at some arbitrary offset that depends on the file's content — a failure that reproduces only for some uploads and looks, from the outside, like corruption. The related sentinel bug is quieter: `while (chunk := src.read(8192)) != "":` against a *binary* source compares `bytes` to `str`, which is never equal, so the loop runs forever on an exhausted stream, spinning on empty reads. The rule is to keep the sentinel in the same type family as the read: `b""` for binary, `""` for text. ### The alternative you should know `iter` has a two-argument form that builds an iterator from a callable and a sentinel, calling it until it returns that sentinel: ```python for chunk in iter(functools.partial(src.read, 8192), b""): digest.update(chunk) ``` This predates `:=` by decades. It is explicit about the sentinel — which is exactly the weakness the truthiness test has — and it gives you an iterator you can hand to anything that consumes one. It costs a `partial` and reads as less direct to most people. Either is defensible; what is not defensible is the duplicated priming read. ### Where the same pattern shows up Capture-and-test in an `if` is the same idea with one iteration. Parsing a filename for its dimensions, you want the match object in the body only when the match succeeded: ```python if m := re.search(r"(\d+)x(\d+)", name): width, height = int(m[1]), int(m[2]) ``` Here truthiness is genuinely the right test, because a failed search returns `None` and a successful match object is always truthy. That is the safest kind of walrus condition: one whose falsy value is `None` and nothing else. All of this behaves the same on Python 3.14 as it has since 3.8.
- When is `while chunk := src.read(n):` the wrong loop condition?Whenever the source can return a falsy value that is not the end of the stream. `read` is safe because its only falsy return is the empty result at EOF, but a queue or generator that legitimately yields `0` or `""` will terminate the loop early and silently drop the rest. In those cases compare against an explicit sentinel, such as `while (item := next(src, SENTINEL)) is not SENTINEL:`.
- What goes wrong if the sentinel type does not match the read?Nothing raises — the comparison is simply never true. `while (chunk := src.read(8192)) != "":` against a file opened in binary mode compares `bytes` with `str`, which never matches, so the loop keeps spinning on empty reads after the stream is exhausted. Match the sentinel to the mode: `b""` for binary reads, `""` for text reads.
- How would you write the same loop without an assignment expression?Use the two-argument form of `iter`, which calls a zero-argument callable until it returns the sentinel: `for chunk in iter(functools.partial(src.read, 8192), b""):`. It is explicit about the terminating value rather than relying on truthiness, works on every Python version, and yields an iterator you can pass elsewhere. The cost is the extra `partial` and slightly more indirection.
saying these in an interview costs you the question
- Treats a falsy chunk as always meaning end of stream
- Keeps a priming read alongside the walrus loop
- Compares a bytes read against a str sentinel
- Says the loop stops when read returns None
- Claims `while True` with a break reads better here
- Assumes the chunk size affects correctness, not just memory