Python's try/else and for/else — the failover that is not a replica
The else is the healthy primary. The except is a different answer.
<!-- hal:authoritative:yaml -->
The else is the healthy primary. The except is a different answer.
§I — Frame
Sunday's Dev lesson named representation. Default dataclass repr prints every field. field(repr=False) mutes one. Coin of that day: the print that is not the value, under the trio coin the credential that is not a key.
Today the Ops tool lists Route 53 health checks and failover pairs. A SECONDARY record is a different answer. It is not a copy of the primary's data. The language surface is one step closer to the interpreter. Python already gave you a clause that people keep reading as a second handler.
Lutz puts four words on the try statement in order: try, except, else, finally (Exception Coding Details). else runs when the try suite did not raise. finally runs either way. Ramalho collects the same else on for and while and puts them in one chapter so you stop treating the word as an if leftover (Fluent Python 2ed, Ch.18, else blocks beyond if).
A reader who sees else after except thinks "another catch." A reader who sees else after for thinks "the loop's extra body." Both readings invent a replica. Coin the correction: the else that is not a handler. Same trio coin as Ops: the failover that is not a replica.
This lesson does not reopen match / case (08-14). Ramalho's Ch.18 also covers match. That cut is spent. This lesson does not reopen weakref.finalize (08-17) or __repr__ (08-23). The clause under the knife is else, and the three statements that grow one.
§II — Language idiom: four suites, one vote
**try is the primary. except is the secondary. else is the proof the primary held.**
def probe(url):
try:
body = fetch(url)
except TimeoutError:
return "failover"
else:
return classify(body)
classify(body) sits in else on purpose. If fetch raises TimeoutError, body was never bound. An except that then called classify(body) would be a NameError dressed as a failover. If fetch succeeds, except must not run. Putting classify after the whole try/except in the function body also works, and that is the form most code reviews prefer. The else form makes the vote visible: the success path is a different suite. It is not a replica of the handler.
Lutz's rule is the one you can recite in a standup. else runs when no exception was raised in try. finally runs on the way out, exception or not, return or not. A return inside except still executes finally before the function actually returns. A return inside else does the same. finally is not a failover. It is the cleanup that is not optional.
**except is typed. else is not a type.**
try:
row = lookup(name)
except KeyError:
row = DEFAULT
else:
audit(name, row)
except KeyError is the secondary answer for one exception class. except (KeyError, TimeoutError) is two secondaries sharing a suite. except Exception is the check that never fails and the finding the Ops census would print. else has no class list. It is not except Success. There is no Success exception. The language already has a suite for "the primary held." Use it or write the success lines after the statement. Do not invent except on a type you control just so the handler and the success path look like twins.
except / else / finally order is fixed. else without except is a SyntaxError. finally may stand alone with try. That last form is a cleanup with no failover, and it is common around files and sockets. Do not add an empty except Exception: raise so you can keep an else. If you have no secondary, you have no else to hang.
**for/else is the search that did not break.**
Ramalho's teaching example is the search. You iterate. You break when you find the thing. The else on the for runs when the iterator exhausted without a break.
def first_healthy(checks):
for check in checks:
if is_healthy(check):
break
else:
return None
return check
The else is "no healthy check." It is not a replica of the loop body. It is not if not checks. An empty iterable takes the else path, because no break fired. A full iterable of unhealthy checks takes the same path. Those two situations are different findings in an ops tool. for/else collapses them. If you must tell them apart, test emptiness first, then search.
continue does not skip else. Only break does. A loop that continues on every unhealthy check and never breaks will run else. That is correct and it surprises people who treat continue as a tiny break.
**while/else is the same vote with a different clock.**
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if ping(primary):
break
time.sleep(0.2)
else:
return use(secondary)
return use(primary)
The else runs when the deadline expired without a break. That is a timeout. It is not a replica of use(secondary) sitting inside the loop. Putting use(secondary) inside the while would call it on every failed ping. The else calls it once, when the primary never answered.
A while True with a break on success and no else is the more common form. Add else when the exhausted condition itself is the event you need to name. If the condition is a counter you also want to log, do not hide the counter in else. Log it before the statement.
**if/else is the only else that is a binary fork.**
Ramalho's chapter title exists because the word else is overloaded. After if, else is the other branch. After for, while, and try, else is "the expected path completed." Teaching all four as "the other thing" is how a failover gets written as a replica: you copy the success body into except and change two lines.
EAFP is not a license to replica the body.
Python culture says "easier to ask forgiveness than permission." That sentence is about try/except versus an if that asks os.path.exists before open. It is not a license to run the same function in both suites. If the primary path is parse(body) and the secondary path is use(cached), those are different functions. A reviewer who pastes parse(body) into except and wraps it in if body else cached has rebuilt the replica. Split the suites. Name the functions differently so a diff cannot look like a copy.
A second cultural collision: "keep try small." Yes. The small try is why else exists. try is the line that can raise. else is the line that must not run if it did. Flattening both into try makes parse able to raise TimeoutError and look like a network miss. Flattening both into the function body after except is legal and often clearer. The illegal move is flattening them into each other.
**Multiple except clauses still share one else.**
try:
body = fetch(url)
except TimeoutError:
return "timeout"
except HTTPError as exc:
return f"http:{exc.code}"
else:
return classify(body)
One success suite. Two secondaries. classify is not copied into either handler. If classify can raise HTTPError, that raise happens outside the handlers and will not be caught by the except HTTPError above it. That is the point of else: success-path exceptions are not retried as if they were the original probe. A replica that called classify inside each except would hide that distinction.
§III — Code worked example: a probe that keeps the suites honest
The Ops census calls get_health_check_status per check. The language problem is the same shape at a smaller scale. You try a primary URL. You classify the body only if the get succeeded. You return a secondary URL if the get timed out. You close the session either way.
import time
from urllib.error import URLError, HTTPError
from urllib.request import urlopen
class ProbeError(Exception):
pass
def read_primary(url, timeout):
try:
with urlopen(url, timeout=timeout) as resp:
status = getattr(resp, "status", None)
body = resp.read(5120)
except HTTPError as exc:
raise ProbeError(f"http:{exc.code}") from exc
except (URLError, TimeoutError, OSError) as exc:
raise ProbeError("unreachable") from exc
else:
if status is None or status < 200 or status >= 400:
raise ProbeError(f"status:{status}")
return body
def first_healthy(urls, timeout=2.0):
errors = []
for url in urls:
try:
body = read_primary(url, timeout)
except ProbeError as exc:
errors.append((url, str(exc)))
continue
else:
return url, body
else:
return None, errors
read_primary puts the status check in else. body and status exist only if urlopen returned. The except suites raise ProbeError and do not inspect body. raise ... from exc keeps the cause. That is chaining, not a replica: the outer type is the one the caller matches, the inner type is the one the traceback still names.
first_healthy uses both try/else and for/else in one function so the two votes stay visible. continue on a failed URL does not skip the for/else. If every URL raises, the for/else returns None, errors. If one URL succeeds, return url, body leaves the for without running else. A break after a success would also skip else; the return is enough.
A wrong version that treats else as a handler looks like this:
def first_healthy_wrong(urls, timeout=2.0):
for url in urls:
try:
return url, read_primary(url, timeout)
except ProbeError:
else:
return None, []
That is not even legal. else cannot sit inside except. The author who wants a replica will instead write:
def first_healthy_flat(urls, timeout=2.0):
last_error = None
for url in urls:
try:
body = read_primary(url, timeout)
return url, body
except ProbeError as exc:
last_error = exc
return None, last_error
That flat form is fine. It is the form you should ship if the team does not know for/else. What you must not ship is a flat form that then also copies return url, body into the except "so we always return something." That copy is the replica. The secondary answer is None, last_error. It is a different shape.
finally belongs on the session, not on the vote.
def read_with_budget(url, timeout, budget):
start = time.monotonic()
try:
return read_primary(url, timeout)
finally:
budget.spent += time.monotonic() - start
finally runs if read_primary returns and if it raises. The budget is not a failover. Putting return secondary in finally would swallow the primary's success. Lutz is explicit: finally is cleanup. Do not hide a secondary answer there.
raise ProbeError(...) from exc is the one from in this lesson. Implicit chaining (raise ProbeError inside except) already sets __context__. from exc sets __cause__ and marks it explicit. from None would suppress the URLError. Suppressing the cause in a probe makes the next on-call guess. Do not.
§IV — Connection to today's Ops lesson
Ops lists HealthChecks and then ResourceRecordSets. The join is a vote: a PRIMARY without a probe is a finding, a PRIMARY without a SECONDARY is a finding, an HTTP check aimed at 10.0.0.0/8 is a finding. None of those findings copy the primary record onto the secondary.
The language vote is the same shape.
try/elseis "the get succeeded; classify the body." Classify is not a replica of theTimeoutErrorhandler.for/elseis "no break; no healthy member." That is not a replica of the loop body that would have returned the first healthy member.except ProbeErroris the secondary answer. It carries a reason, not a copied body.
If the census script from Ops grows a per-check HTTP probe (you should not need one; get_health_check_status is the fleet's vote), put the body parse in else. Put the timeout in except. Do not parse a body you never read.
EvaluateTargetHealth on an alias is the language else written by Route 53. The alias is the primary name. The target reports healthy or not. There is no copied record. The Python that prints evaluate=True is describing a vote, not a clone. A function that then constructs a secondary record in the same suite that printed the vote has spent the coin.
§V — Prior-lesson reach
08-23 stays __repr__ and field(repr=False). Today's else does not change what prints. A ProbeError interpolated into a log line still goes through __repr__. Keep the error short (http:503, unreachable). Do not attach body to the exception "for later." Later is a log.
08-20 stays identity versus equality. else is not an equality test. Two different URLs can return bodies that == each other. The vote is "this get succeeded," not "this body equals the last one." A failover that compares bodies and stays on the primary because the secondary looks the same is a replica test. Do not write it.
08-17 stays weakref.finalize and the signal that must still fire. finally is the cousin you already have in the syntax. Finalize is for objects you do not own the with for. If you have a try, prefer finally or a context manager. Do not register a finalize inside else because the success path felt like a constructor.
08-14 stays match / case on the subject. You may match str(exc) after a ProbeError. You must not use match as a replica of except for control flow you already have. except ProbeError is the typed door. match is the shape door. Spent cut, named so this chapter of Ramalho does not pull it back.
08-11 stays async context managers and cancellation. async with has no else. An async for does grow else, with the same break rule. If you port first_healthy to asyncio, the else still means "no break." Cancellation raises. That raise skips else on try and still runs finally. Do not swallow CancelledError to force the else path.
07-24 was a PCAP cert lesson on exception types and except order. That fire stays the taxonomy. This fire is the suite that is not in the taxonomy: else. A reviewer who says "we already did exceptions" is naming the handler. Point them at the other clause.
§VI — Closing
else on try is the healthy primary. except is a different answer. else on for and while is "the search never broke." finally is cleanup, not failover. if/else is the only binary fork. The other three are completion votes.
Ramalho put them in one chapter so the word would stop meaning "the other branch." Lutz put them in one statement so the order would stop being a guess. Use the clause when the vote is the point. Use the flat form when the team will misread it. Never copy the success body into except so both suites look busy.
Examine the next try in the Route 53 census. If classify sits inside except TimeoutError, the coin is already spent. Move it to else, or move it below the statement. Leave the handler thin.
Related
- Prior arc: __repr__ / field(repr=False) (2026-08-23)
- Language hub: Cross-References/dev-languages/Python
- Grounding tome: Fluent Python, 2nd ed. (Ch.18 else blocks beyond if) · Learning Python (Lutz) (Exception Coding Details)