Python Part I · Module 9 · Conditional Statements
Comparison and Logical Conditions
You already own every operator this chapter uses. Module 3 taught comparisons, and, or, not, and membership as machinery for building expressions, and at the time the result was something to print. Now the result selects which code runs, and that raises the stakes on exactly the same syntax: a wrong expression produces a wrong number you can see, while a wrong condition runs the wrong block and produces a plausible answer you cannot. This chapter is about writing conditions that say what you mean — and about three specific conditions that are perfectly valid Python, raise nothing at all, and quietly mean something else.
Foundation16 min readPythonPart IFundamentalsConditionalsBoolean Logic
Part I · Module 9 · Chapter 9.3 · Comparison and Logical Conditions
1. Comparing Two Values
The most common condition compares something to a known value. Chapter 3.4 covered the six operators; here they are doing their real job:
status = "FAIL"
errors = 4
if status != "PASS":
print("not a clean run")
if errors >= 4:
print("at least four errors")not a clean run
at least four errorsNothing has changed about the operators. status != "PASS" produces True, exactly as it would in a print. What is new is that the value is consumed by the if rather than displayed.
Two comparisons are worth pausing on, because both have cost people time.
Equality across numeric types works the way the mathematics does:
print(1 == 1.0)
print(4210 == int("4210"))True
TrueEquality between a string and a number does not:
print("4210" == 4210)Falseseed = "4210"
if seed == 4210:
print("matched")
else:
print("did not match")did not matchThe string "4210" and the integer 4210 are different values, and no amount of looking alike changes that. This matters because text read from a file or a log arrives as a string: a condition comparing it to a number is always false, silently. Chapter 2.11 covered the conversion; the condition is where forgetting it shows up.
2. = Is Not ==
Assignment and comparison are different operators, and using the first where the second belongs is the classic beginner error. Python refuses outright:
status = "FAIL"
if status = "PASS":
print("yes") File "bad.py", line 3
if status = "PASS":
^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?Modern Python recognises the mistake and names the likely fix. = binds a name to a value — it is a statement, and a statement cannot be a condition. == compares two values and produces True or False, which is what a condition needs.
3. Combining Conditions
and, or and not combine conditions into one, as Chapter 3.5 established.
errors = 0
warnings = 3
if errors == 0 and warnings > 0:
print("clean, but review the warnings")clean, but review the warningsA and Bis true when both are true.A or Bis true when at least one is true.not Ais true whenAis false.
One precedence fact is worth memorising because it changes answers: and binds more tightly than or, so A or B and C means A or (B and C).
print(True or False and False)
print((True or False) and False)True
FalseSame three values, opposite results. Chapter 3.8 made the general case; the rule in practice is simpler than the precedence table:
4. Short-Circuit: and and or Stop Early
and and or do not always evaluate both sides. and stops at the first operand that is false — the answer cannot change after that. or stops at the first that is true.
This is not a performance footnote. It is the mechanism that makes a guard work.
denominator = 0
if denominator != 0 and 100 / denominator > 5:
print("large ratio")
else:
print("skipped the division")skipped the divisiondenominator != 0 is False. The and can stop there, so 100 / denominator is never evaluated — and that division would have raised.
The proof is to swap the two halves and change nothing else:
denominator = 0
if 100 / denominator > 5 and denominator != 0:
print("large ratio")ZeroDivisionError: division by zeroIdentical operands, identical operator, opposite outcome. In the first version the guard runs first and stops the chain; in the second the dangerous expression runs before anything can protect it.
Put the condition that makes the next one safe on the left. That ordering is the whole technique, and you will use it constantly once Chapter 9.4 pairs it with dictionary lookups.
5. Membership Conditions
in asks whether a value is present in a collection. It reads like English and works on every collection you have met:
line = "UVM_ERROR axi_smoke"
tests = ["smoke", "reset"]
failed = {"reset", "crc"}
print("ERROR" in line)
print("smoke" in tests)
print("crc" in failed)True
True
Trueline = "UVM_ERROR axi_smoke"
if "UVM_ERROR" in line:
print("this line reports an error")this line reports an errorOn a dictionary, in checks the keys — the rule Chapter 8.2 established, arriving where it finally matters:
status = {"smoke": "PASS", "reset": "FAIL"}
print("reset" in status)
print("FAIL" in status)
print("FAIL" in status.values())True
False
True"FAIL" in status is False even though "FAIL" is plainly in there — as a value, not a key. If you mean to search the values, say so with .values().
Membership also replaces a chain of equality tests, and reads better than one:
status = "WARN"
if status in {"PASS", "WARN"}:
print("acceptable")acceptableThat is cleaner than status == "PASS" or status == "WARN", and Section 7 shows what happens when someone tries to shorten it the wrong way.
6. Checking for None
None is the value that means no value yet. Chapter 3.6 introduced is as the identity operator, and None is the case it exists for:
result = None
if result is None:
print("no result recorded yet")no result recorded yetWrite is None, not == None. There is exactly one None object in a running program, so asking whether a value is that object is both the direct question and the conventional spelling. Every Python reviewer expects is None, and seeing == None is a reliable signal that the author was unsure.
7. The or Trap
Here is a condition that looks like a natural shortening of "status is PASS or WARN", is valid Python, and is wrong:
status = "FAIL"
if status == "PASS" or "WARN":
print("accepted")
else:
print("rejected")acceptedThe status is "FAIL" and the program accepted it. Worse, it will accept everything — there is no input for which that condition is false.
The reason is that or combines two complete conditions, and the second one here is not a comparison at all. Python reads the line as:
status = "FAIL"
print((status == "PASS") or "WARN")
print(bool("WARN"))WARN
TrueThe left side is False, so or moves to the right side, which is the bare string "WARN" — and a non-empty string counts as true. The condition is therefore true no matter what status contains. (Chapter 9.5 covers why a string counts as true at all; here it is enough that it does.)
Both repairs state the second condition in full:
status = "FAIL"
if status == "PASS" or status == "WARN":
print("accepted")
else:
print("rejected")rejectedstatus = "FAIL"
if status in {"PASS", "WARN"}:
print("accepted")
else:
print("rejected")rejectedThe membership form is the better of the two: one mention of status, and adding a third acceptable value means adding one word.
8. Conditions That Can Never Be True
A compound condition can be contradictory, and Python will evaluate it faithfully forever.
temperature = 50
if temperature > 80 and temperature < 20:
print("outside the safe range")
else:
print("inside the safe range")inside the safe rangeThe branch is unreachable for every possible temperature: no number is both above 80 and below 20. The author meant outside the range, which is or:
temperature = 50
if temperature > 80 or temperature < 20:
print("outside the safe range")
else:
print("inside the safe range")inside the safe rangetemperature = 95
if temperature > 80 or temperature < 20:
print("outside the safe range")
else:
print("inside the safe range")outside the safe rangeThe rule to carry: and narrows, or widens. Two conditions on the same quantity joined by and describe the overlap between them, and if they do not overlap the result is always false. Joined by or they describe everything either one covers.
For inside a range, Python allows the chained form, which is unambiguous and reads like the mathematics:
temperature = 50
print(20 <= temperature <= 80)
print(80 < temperature < 20)True
False9. not, and Where Negation Goes Wrong
not inverts a condition, and it binds more tightly than and and or but less tightly than a comparison:
a = 3
b = 4
print(not a == b)
print(a != b)True
Truenot a == b is read as not (a == b), not as (not a) == b. It agrees with a != b — and a != b is what to write. Negating a comparison when a negative comparison already exists adds a step for the reader with nothing in return.
Where not earns its place is in front of something that is not a comparison:
rerun_enabled = False
if not rerun_enabled:
print("reruns are off")reruns are offReserve not for inverting a whole condition, especially a compound one or a membership test:
tests = ["smoke", "reset"]
if "burst" not in tests:
print("burst is not in the suite")burst is not in the suitePython spells that not in, as one operator, which reads better than not ("burst" in tests) and means the same thing.
10. A Condition Table
Collected for reference — each row is a question and the condition that asks it:
| Question | Condition |
|---|---|
| are these equal | status == "PASS" |
| are these different | status != "PASS" |
| is it at or past a threshold | coverage >= 90 |
| is it inside a range | 20 <= temp <= 80 |
| is it outside a range | temp < 20 or temp > 80 |
| are both true | errors == 0 and finished |
| is either true | fatal or errors > 0 |
| is it one of several values | status in {"PASS", "WARN"} |
| is it present in a collection | "UVM_ERROR" in line |
| is it absent | "burst" not in tests |
| is it a key of this dictionary | "reset" in status |
| is there no value yet | result is None |
11. Predict, Run, Observe
Four conditions. Predict each, then read the output. Two of them are bugs.
# A
errors = 0
warnings = 0
print(errors == 0 and warnings == 0)
# B
seed = "4210"
print(seed == 4210)
# C
status = "FAIL"
print(status == "PASS" or "PASS")
# D
runtime = 3600
print(runtime > 7200 and runtime < 1800)The verified output:
True
False
PASS
FalseA — both halves are true, so the result is True. Nothing surprising; it is here as the baseline.
B — False, because a string is never equal to an integer. If this were a condition, its branch would never run.
C — not True or False at all. or returns the first operand that counts as true, and that is the string "PASS" itself. Used as a condition it would behave as true for any status — the Section 7 bug, printed rather than branched on.
D — False, and false for every possible runtime. Nothing is both above 7200 and below 1800.
12. Exercises
Exercise 1 — write the condition. Write the if line only, for each: (a) the log line contains UVM_FATAL (b) coverage is below 90 (c) the run finished and recorded no errors (d) the status is one of FAIL, FATAL or TIMEOUT (e) "reset" has no entry in the status dictionary (f) result has not been set yet.
Exercise 2 — find the always-true condition. One of these is true for every input. Identify it, explain why in one sentence, and repair it.
if tool == "vcs" or "xrun":
...
if errors > 0 or warnings > 0:
...
if status in {"PASS", "FAIL"}:
...Exercise 3 — find the never-true condition. Same three-part task for this set.
if width >= 8 and width <= 4:
...
if name != "clk" and name != "rst":
...
if "FAIL" in {"smoke": "FAIL"}:
...Exercise 4 — order the guard. values is a list and index an integer. Write a condition that is true when index is a valid position in values and the item there equals "FAIL". Then write it in the wrong order and run both with values = [] and index = 0. Record what each does.
Exercise 5 — the string-number trap. seed_text = "4210". Write two conditions testing whether the seed is 4210: one that is always false, and one that works. Explain the difference in one sentence.
Exercise 6 — parenthesise. For fatal = False, errors = 2, reviewed = False, evaluate fatal or errors > 0 and reviewed by hand using the precedence rule, then verify. Then write the two parenthesised versions and give the result of each.
Exercise 7 — key or value. For status = {"smoke": "PASS", "reset": "FAIL"}, predict each of "smoke" in status, "PASS" in status, "PASS" in status.values() and "smoke" in status.values(), then verify all four and explain the pattern in one sentence.
Exercise 8 — simplify. Rewrite each more directly, and say what you removed: (a) if not count == 0: (b) if status == "PASS" or status == "WARN" or status == "INFO": (c) if not ("burst" in tests): (d) if result == None:
13. Summary
- A condition is an expression, and every operator from Module 3 works unchanged inside one.
- A string is never equal to a number.
"4210" == 4210isFalse, which makes conditions over text read from a file silently never match. =cannot appear in a condition. Python raises aSyntaxErrorand suggests==.andbinds more tightly thanor. Parenthesise any condition that mixes them.andstops at the first false operand,orat the first true one. Swapping the halves of a guard turns a clean skip into aZeroDivisionError.- Put the condition that makes the next one safe on the left.
inchecks membership, and on a dictionary it checks keys —"FAIL" in statusis false when"FAIL"is a value.status in {"PASS", "WARN"}beats a chain of==, and extends by one word.- Use
is None, not== None. if x == "A" or "B":is always true. Each side of anormust be a complete condition on its own.andnarrows,orwidens. Two non-overlapping conditions joined byandcan never be true.- Prefer
a != btonot a == b, andnot into a negatedin.
The next chapter puts two decisions together, and asks when a nest is the right shape and when one combined condition is clearer.
Where this fits
Part of the Python curriculum.
