Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 if line
  • 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
Two rows of three blocks. The upper row, shown muted, is the RTL model: both branches exist in silicon, then an arrow labelled select to a multiplexer that picks one output, then an arrow labelled always to a block saying both paths are powered at all times. The lower row is the Python model: the condition is evaluated once, then an arrow labelled chooses to a block where only the taken statements execute, then an arrow labelled then to a block where execution continues after the statement.RTL: both branchesbuilt in silicon togetherRTL: the selectpicks which result is usedRTL: continuouslyboth paths stay poweredPython: the conditionevaluated once, on arrivalPython: one branchonly its statements runPython: afterwardsexecution carries onselectalwayschoosesthen12
Figure 1 — the same conditional under two execution models. Above, the RTL reading: both branches are built and continuously driven, and a select picks which result is used. Below, what Python actually does: the condition is evaluated once, one block of statements runs, and execution moves on. The untaken block is not a slower path — it is not a path at all.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors = 0

if errors > 0:
    print(1 / 0)

print("the program finished")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the program finished

1 / 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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
errors = 3

if errors > 0:
    print(1 / 0)

print("the program finished")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ZeroDivisionError: division by zero

Identical 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors = 0

if errors > 0:
    verdict = "FAIL"
else:
    verdict = "PASS"

print(verdict)

errors = 99
print(verdict)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS
PASS

errors 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nothing to report
both values were built, because both are statements

a 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors = 0

if errors > 0:
    a = "expensive report for " + str(errors)
    print(a)
else:
    print("nothing to report")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nothing to report

Now 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

QuestionRTL conditionalPython if
What does it describestructure that existsan action taken once
How many branches exist after elaborationbothboth, as text
How many branches executeboth are drivenexactly one
When is the condition evaluatedcontinuouslyonce, on arrival
Does changing the input update the resultyes, it re-propagatesno, the value stays
What does an unused branch costarea and powernothing
Can a wrong branch go unnoticedit is always drivenyes, 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: finished
B: finished
C: finished 0

A — 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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
n = 50
if n > 10:
    print(1 / 0)
print("finished")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ZeroDivisionError: division by zero

9. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 if is 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 / 0 in 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. verdict holds 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.