Combining conditions
Real rules are rarely a single test. A ticket sale needs the buyer to be an adult AND have ID. An alert fires when a reading is too low OR too high. Logical operators let one condition state the whole rule, instead of stacking nested ifs that each check one piece.
Three logical operators combine True/False values:
| Expression | True when |
|---|---|
a and b | both are true |
a or b | at least one is true |
not a | a is false |
age = 20 has_id = True age >= 18 and has_id # True: both hold age < 18 or age > 65 # False: neither holds not has_id # False
Python also lets you chain comparisons like math notation: 0 <= age <= 30 means age is between 0 and 30, no and needed.
Precedence: not binds tightest, then and, then or. So a or b and c means a or (b and c). When mixing them, use parentheses and spare the reader the puzzle.
Each operator on real values
The four lines below cover and, or, not, and a chained comparison, using a 20-year-old buyer who does carry ID.
age = 20 has_id = True print(age >= 18 and has_id) print(age < 18 or age > 65) print(not has_id) print(0 <= age <= 30)
Output
True False False True
The first line is True because both halves hold, an adult who also has ID. The second asks whether the age is outside the middle band and gets False, since 20 is neither under 18 nor over 65, and an or needs at least one side to succeed. The third simply flips has_id. The last line is the chained form, which Python reads as 20 being at least 0 and at most 30, both of which are satisfied.
With x = 5, the expression not (x > 3 and x < 10) evaluates to False.
The parentheses are resolved first, exactly as in arithmetic. Inside them, x > 3 is True and x < 10 is True, so the and produces True. The not then inverts that single result to False.
Reading these expressions inside-out is the reliable habit. Working outside-in tempts you to apply not to only the first comparison, which would be a different expression entirely and would need to be written (not x > 3) and x < 10.
The leap year rule is a good test of operator grouping, because it genuinely needs both and and or. A year qualifies when it divides by 4 and not by 100, or when it divides by 400 regardless.
year = 2000 leap = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 print(leap)
Output
True
Divisibility is tested with %, so year % 4 == 0 asks whether dividing by 4 leaves no remainder. The parentheses group the and portion so that the or is applied last, which matches how the rule is stated. For 2000 the first group is False, since 2000 does divide by 100, but the final clause rescues it because 2000 also divides by 400. Trying 1900 shows the other side of the rule: it fails the first group for the same reason and does not divide by 400, so the whole expression is False, and 1900 was indeed not a leap year.
Store rules translate into the same shape. Free shipping applies when the total reaches a threshold or when the customer holds a membership.
total = 62 is_member = False free_shipping = total >= 50 or is_member print(free_shipping)
Output
True
The first half is True because 62 reaches the 50 threshold, and an or needs only one side, so the non-member status makes no difference here. Notice that is_member stands in the expression on its own, without any == True attached, since it already holds a boolean. Comparing a boolean to True is redundant and reads worse. Storing the whole rule in one well-named variable also pays off, because later code can ask about free_shipping without repeating the logic.