Skip to main content
Learning Center
Python Programming

Module 2: Decisions and Validation

Comparisons and Boolean Logic

What a comparison actually produces, how and/or/not combine results, and the truthiness rules that make an empty value quietly count as no.

Lesson 7 of 46 in the recommended order · About 25 min (estimate)

On this page

Outcome

By the end of this lesson you can predict the value of any comparison or combined boolean expression by hand, and you can explain why a missing value sometimes counts as "no" without anyone writing False.

Why it matters

Every decision a program makes is a boolean underneath. Before you can read branching code, you have to be able to read the condition that drives it, and the conditions are where the subtle bugs live: an or that should have been an and, a > that should have been >=, a check that passes because an empty field counts as absent rather than as a real answer.

This is also the module where the review assistant stops being a printout and starts making a judgement, so the reasoning has to be something you can defend, not something that happens to work on one record.

Concept

A comparison produces a bool, always. == asks whether two values are equal, != whether they differ, and <, <=, >, >= order them. == compares values, not identity: 240000 == 240000.0 is True even though one is an int and one a float.

Three operators combine booleans:

  • and is True only when both sides are.
  • or is True when either side is.
  • not flips its single operand.

not binds tightest, then and, then or. So a or b and c means a or (b and c), which is rarely what a hurried author meant. Parenthesise when the reader would have to think.

Python evaluates and/or lazily, left to right, stopping as soon as the answer is decided. False and something_expensive never evaluates the right-hand side at all. That is a feature you will rely on later to check that a value exists before using it.

Chained comparisons read the way maths does: 1000 <= value <= 500000 means both comparisons must hold, and value is evaluated once.

Finally, truthiness. Python lets any value stand in for a condition, and the following count as false: False, None, 0, 0.0, "" (the empty string), and empty collections. Everything else counts as true. This is convenient and it is a trap: a check written as "did we get a set-aside?" quietly answers "no" for an empty string, which may be right, and also answers "no" for a legitimate value of 0, which usually is not.

Read the code

estimated_value = 96500
minimum_value = 100000
status = "Active"
set_aside = ""

value_ok = estimated_value >= minimum_value
status_ok = status == "Active"
has_set_aside = bool(set_aside)

print(value_ok, status_ok, has_set_aside)
print(value_ok and status_ok)
print(value_ok or status_ok)
print(not has_set_aside)

The first three names each capture one question, and each holds a bool after the line runs. Naming them is doing real work: value_ok says what the comparison means, while estimated_value >= minimum_value only says what it computes.

bool(set_aside) makes the truthiness conversion explicit rather than leaving it implied inside a condition. When an empty string is a meaningful state in your data, spelling out the conversion is the difference between a reader trusting the code and a reader guessing at it.

Predict the output

Predict all four printed lines.

Check your prediction
False True False
False
True
True

96500 >= 100000 is False. status == "Active" is True, an exact match including case. bool("") is False, because the empty string is one of the falsy values.

The and line is False because one side is. The or line is True because one side is. not has_set_aside is True, which reads as "this record has no set-aside", and is correct here.

Modify the code

Change set_aside = "" to set_aside = "None", a real value some publishers use to mean "not reserved". Predict which printed values change.

What changes, and why

The third value on the first line becomes True, and the last line becomes False.

"None" is a four-character string, not Python's None, and a non-empty string is truthy. So the record now looks to your code as though it has a set-aside category, when the publisher meant the opposite.

This is not a Python quirk to memorise; it is a data-modelling problem you will meet constantly. Somewhere between the publisher and your check, a human decision, "no set-aside", was encoded as text, and only a rule you write on purpose can decode it. Module 6 turns this into an explicit normalisation step.

Debug the bug

An assistant was asked for a check that accepts an opportunity when the value is in range and the status is active. It produced this and claimed the record below is rejected.

estimated_value = 96500
status = "Closed"

acceptable = estimated_value >= 100000 or status == "Active" and estimated_value < 500000
print(acceptable)
What's actually wrong

It prints False here, so the claim happens to hold for this record, and the logic is still wrong.

and binds tighter than or, so Python reads the expression as:

(estimated_value >= 100000) or ((status == "Active") and (estimated_value < 500000))

The intended rule was almost certainly "value is at least the minimum and below the ceiling and the status is active". As written, any record with a large enough value is accepted regardless of status, because the first branch of the or short-circuits the rest. Set estimated_value to 240000 and status to "Closed" and it returns True.

The lesson is not "memorise precedence". It is that a condition long enough to need precedence rules is a condition that should be broken into named parts:

big_enough = estimated_value >= 100000
under_ceiling = estimated_value < 500000
is_active = status == "Active"

acceptable = big_enough and under_ceiling and is_active

Same behaviour, and now a wrong answer is traceable to one named line.

Try it yourself

One record, three independent conditions. Name each check, print all three, then print whether all three hold. Exactly one of them fails, and naming them is what lets you see which.

Loading this exercise…

Practical challenge (optional)

Optional: change the closing-window check from > 7 to >= 7 and work out, without running it, which record values now change verdict. Then run it and confirm. Boundary conditions are where matching rules go wrong in practice, and the habit of asking "what happens exactly at the boundary" is worth more than any single rule you will write.

Sign in to track your progress on this exercise.

AI collaboration

Checkpoint

Give the value of each expression:

  1. not (3 > 5)
  2. "" or "fallback"
  3. 0 and 1 / 0
  4. 100 <= 100 <= 200
  5. "Active" == "active"
Answers
  1. True. 3 > 5 is False, and not flips it.
  2. "fallback". or returns the first truthy operand, not a boolean; the empty string is falsy, so the second value is the result. This is the common idiom for a default.
  3. 0. and stops at the first falsy operand and returns it, so the division by zero is never evaluated. Short-circuiting is why this does not raise.
  4. True. Both comparisons hold, and <= includes the boundary.
  5. False. String comparison is case-sensitive, which is why normalising case before comparing is a step you will keep writing.

Sign in to track your progress on this exercise.

Summary and next step

Comparisons produce booleans, and/or/not combine them with a precedence worth parenthesising around, short-circuiting means the right-hand side may never run, and falsy values let a missing field silently answer "no". Next: turning those conditions into branching code that stays readable as the rules grow.

learning.goultergroup.com

The interactive parts of this page have not loaded. Reading and links still work; reload the page to try again.