Python Part I · Module 9 · Conditional Statements
How an if Actually Runs
You can now write conditionals that work. This chapter is about what one does when the program runs, and it exists because engineers who think in hardware arrive with a model that is precisely wrong in a way that never produces an error message. In RTL, a conditional describes structure: both branches are built, both are continuously driven, and a select decides which result is used. Every instinct that follows from that — about cost, about when things happen, about whether a result stays correct — is false in Python. The correction is not subtle and it is demonstrable in three lines, so this chapter demonstrates it rather than asserting it.
Foundation14 min readPythonPart IFundamentalsConditionalsExecution Model
Part I · Module 9 · Chapter 9.7 · How an if Actually Runs
1. The Model That Comes From Hardware
Write a conditional in RTL and you are describing a circuit:
always_comb
if (errors != 0)
status = FAIL;
else
status = PASS;Both assignments exist in the synthesised design. Both input paths are routed, both are powered, and a selector decides which value reaches status. The if did not cause anything to happen in sequence — it described a structure that holds continuously, and the structure contains both branches at all times.
That model is correct for hardware and it is the natural reading to bring to Python. It is also wrong in every particular.
2. The Second Transfer Error
Chapter 2.3 corrected the first one: Python's = happens once, at the moment the line runs, and does not describe an ongoing relationship between two names. Writing errors = total copies a value at that instant; changing total afterwards leaves errors alone.
This is the same mistake in a different construct, and it has the same root. RTL describes a structure that exists continuously. Python describes a sequence of actions, each of which happens once, in order, when control reaches it.
Applied to a conditional, that means:
- the condition is evaluated once, when execution arrives at the
ifline - one branch is chosen
- the statements in that branch run
- the statements in the other branch do not run at all — not cheaply, not quickly, not at all
- execution continues past the whole statement
3. The Proof
Claims about what does not happen are hard to believe and easy to test. Put something in the untaken branch that could not possibly survive being run.
errors = 0
if errors > 0:
print(1 / 0)
print("the program finished")the program finished1 / 0 is in the program. It is syntactically fine, so Python accepted the file. And it did not raise, because errors > 0 was false and the block was never entered.
Now change the one integer:
errors = 3
if errors > 0:
print(1 / 0)
print("the program finished")ZeroDivisionError: division by zeroIdentical three lines. The only difference is which branch the condition selected, and that difference is the difference between a clean finish and a crash.
4. What Follows From It
Several instincts need updating, and all of them follow from Section 3 rather than being separate facts.
Work in a branch costs nothing when the branch is not taken. In hardware, a path you rarely use still occupies area and burns power. In Python, a branch that handles a rare failure case costs exactly nothing on every run that does not fail. Writing careful, thorough handling for an unusual case has no runtime price for the usual one.
Both branches cannot both affect the result. There is no moment at which both have run. Whatever a branch did, the other one did not do.
Only one branch needs to be correct for a given input — which is also why the other one can stay wrong for a long time without anyone noticing. A branch that never runs is never tested.
Order matters, because this is a sequence. Statements before the if have already happened; statements after it have not yet. In RTL there is no "before" and "after" within a combinational block.
5. The Condition Is Not a Standing Relationship
The clearest way to feel the difference is to change the input after the decision has been made.
errors = 0
if errors > 0:
verdict = "FAIL"
else:
verdict = "PASS"
print(verdict)
errors = 99
print(verdict)PASS
PASSerrors is now 99 and verdict is still "PASS".
In RTL, errors changing would propagate: the condition re-evaluates, the mux re-selects, and the output changes within a delta. Here, the if ran once — at the moment execution reached line 3 — and produced a value. Nothing is watching errors. Nothing re-runs.
6. Where the Laziness Stops
The converse is worth stating, because over-applying the rule produces a different wrong model.
Laziness belongs to the branch, not to anything that resembles a choice. Arguments to a call are all evaluated before the call happens, and ordinary statements before an if have already run:
errors = 0
a = "expensive report for " + str(errors)
b = "nothing to report"
print(a if errors > 0 else b)
print("both values were built, because both are statements")nothing to report
both values were built, because both are statementsa was constructed even though it was not used. Nothing about the conditional expression on line 6 reaches backwards to line 3 — by the time Python evaluates the condition, both names already hold strings.
To actually skip the work, the work must be inside the branch:
errors = 0
if errors > 0:
a = "expensive report for " + str(errors)
print(a)
else:
print("nothing to report")nothing to reportNow the construction never happens.
The rule in one line: Python skips the statements inside an untaken block. Statements that merely appear earlier in the file have already run, whatever they were preparing for.
7. A Side-by-Side Reading
| Question | RTL conditional | Python if |
|---|---|---|
| What does it describe | structure that exists | an action taken once |
| How many branches exist after elaboration | both | both, as text |
| How many branches execute | both are driven | exactly one |
| When is the condition evaluated | continuously | once, on arrival |
| Does changing the input update the result | yes, it re-propagates | no, the value stays |
| What does an unused branch cost | area and power | nothing |
| Can a wrong branch go unnoticed | it is always driven | yes, until an input reaches it |
The last row is the practical one. In hardware, a branch that is wrong is wrong all the time and shows up in simulation. In Python, a branch that is wrong is silent until the day an input selects it — which is why Chapter 9.1 spent so long on reachability and boundaries.
8. Predict, Run, Observe
Three programs, each containing a division by zero. Say for each whether it finishes or raises.
# A
n = 5
if n > 10:
print(1 / 0)
print("A: finished")
# B
n = 5
print(1 / 0 if n > 10 else "B: finished")
# C
n = 5
value = 1 / 0 if n > 10 else 0
print("C: finished", value)All three finish:
A: finished
B: finished
C: finished 0A — the block is not entered.
B — the untaken side of a conditional expression is not computed either; Chapter 9.6 showed the same thing.
C — identical reasoning; the division is the untaken half of the expression, so value is 0.
Now one that does not finish, to confirm the difference is the branch and not the division:
n = 50
if n > 10:
print(1 / 0)
print("finished")ZeroDivisionError: division by zero9. Exercises
Exercise 1 — predict and verify. For each, say whether it raises: (a) if False: print(1/0) (b) print(1/0 if False else "ok") (c) x = 1 / 0 on its own line, followed by print("ok" if False else x) (d) if True: with print(1 / 0) indented beneath it.
Exercise 2 — the stale verdict. Extend Section 5's program so that after setting errors = 99 it prints the correct verdict. Explain in two sentences what you had to write and why nothing shorter would do.
Exercise 3 — translate the instinct. For each hardware instinct, write the Python correction in one sentence: (a) "that branch costs area even when unused" (b) "both sides settle, then the mux picks" (c) "when the input changes the output follows" (d) "an unused branch is still exercised every cycle".
Exercise 4 — where does the work happen. Rewrite this so the long string is built only when it is needed, and explain what changed.
summary = "errors: " + str(errors) + ", warnings: " + str(warnings)
if errors > 0:
print(summary)Exercise 5 — the untested branch. Write a conditional whose else branch contains an obvious error, choose an input for which the program runs perfectly, and then choose the input that exposes it. Say in one sentence what this implies about testing conditionals.
Exercise 6 — count the executions. For the program in Section 5, state exactly how many times the condition errors > 0 is evaluated, and justify your answer by pointing at the lines.
Exercise 7 — argument versus branch. Explain in three sentences why print(expensive if flag else cheap) skips building one value, while a = expensive on an earlier line does not. Verify both with a division by zero.
Exercise 8 — write the comparison. Without looking at Section 7, reproduce as many rows of that table as you can from memory, then check. For any row you missed, write the one-line experiment that would have shown it to you.
10. Summary
- An
ifis not a mux. It does not evaluate both branches and select one result; it runs one branch and never touches the other. - This is the second transfer error, and it shares a root with Chapter 2.3's: RTL describes a structure that holds continuously, Python describes actions that happen once, in order.
- The condition is evaluated once, when execution reaches the line.
1 / 0in an untaken branch does not raise. The same three lines raise when the condition selects that branch — which proves the statements are skipped rather than evaluated and discarded.- An untaken branch costs nothing, so thorough handling of a rare case is free on every run that does not hit it.
- Changing the input afterwards does not update the result.
verdictholds a value, not a rule. - A wrong branch stays silent until an input reaches it, which is why reachability and boundary testing matter so much.
- Laziness belongs to the branch. Work on an earlier line has already happened; to skip it, put it inside the block.
The last chapter of this module takes a long elif chain that dispatches on a value and replaces it with a construct built for exactly that job.
Where this fits
Part of the Python curriculum.
