HPy · Unit 1 Review

1.4 Conditionals

Basic Programming Constructs · branching logic, truth tables, and tracing the path your code actually takes

FRQ · Warm-Up

What can a conditional do?

In your own words: what does an if statement let a program do that it couldn't do by just running one line after another?

1.4.1 if Statements

Running code, only sometimes

A conditional, often called an if statement, runs a chunk of code only when some boolean test is True. This section covers conditionals in Python.

The General Form

Test, then maybe run the body

Python evaluates the TEST. If, and only if, it is True, Python runs the indented BODY. If the test is False, Python simply skips it and moves on to whatever comes next.

main.py
if TEST:
    BODY
Try This

One branch, two outcomes

The test is n < 0. Since return exits the function immediately, line 4 only runs when the test was False.

main.py
1def signString(n):
2    if n < 0:
3        return 'Negative'
4    return 'Non-negative'
5 
6print(signString(-5))
7print(signString(42))
8print(signString(0))
console
'Negative' 'Non-negative' 'Non-negative'
Body With Multiple Lines

Some lines always run

The body of a conditional can be several lines, all indented the same amount. 'A' prints on every call, but 'B' only prints when the if body actually runs.

main.py
1def signString(n):
2    print('A')
3    if n < 0:
4        print('B')
5        return 'Negative'
6    print('C')
7    return 'Non-negative'
8 
9print(signString(-5))
10print(signString(42))
11print(signString(0))
console
A B Negative A C Non-negative A C Non-negative
Body On The Same Line

A shortcut, used sparingly

When the condition is simple and the body is one short line, you can put both on the same line. This can save space, but it also makes code harder to scan.

main.py
if x > 10: print('yes!')
Only use this for very simple cases. If your condition or body has any real complexity, keep them on separate, indented lines.
MCQ

What does this actually print?

main.py
def f():
    if True:
        print('a')
    print('b')

print(f())

What is printed by this code?

  • A a
  • B a then b
  • C a then b then None
  • D a then None

f has no return, so once it prints a and b, the outer print(f()) still prints whatever f returned: None.

Recap

if statements

  • An if TEST: only runs its indented body when TEST is True; otherwise Python skips straight past it.
  • The body can be one line or many, but every line must be indented the same amount.
  • A one-line if TEST: BODY shortcut exists, but save it for the simplest cases.
1.4.2 if-else Statements

Covering the other case

A plain if only handles the True case. An if-else statement adds a second body that runs whenever the condition is False, so every possibility is covered in one clean structure.

Try This

Same logic, one structure

Python runs the else body only when the if's condition was False. Exactly one of the two bodies runs, never both, never neither.

main.py
1def signString(n):
2    if n < 0:
3        return 'Negative'
4    else:
5        return 'Non-negative'
6 
7print(signString(-5))
8print(signString(42))
9print(signString(0))
console
'Negative' 'Non-negative' 'Non-negative'
MCQ

Which body runs when the test is False?

main.py
def f():
    if False:
        print('a')
    else:
        print('b')

print(f())

What is printed?

  • A a then b
  • B a then None
  • C None
  • D b then None
Recap

if-else statements

  • An else body runs only when the paired if's condition was False.
  • Exactly one of the two bodies runs on any given call, so an if-else always covers both possibilities.
1.4.3 Nested Conditionals

A conditional inside a conditional

What if there are three cases instead of two? Say we want negative, positive, and zero. A nested conditional, an if inside the body of another if, handles it.

Try This

Three cases, two levels deep

Python only enters the else body when n < 0 was False. Only then does it check the nested n > 0.

main.py
1def signString(n):
2    if n < 0:
3        return 'Negative'
4    else:
5        # n is either positive or zero
6        if n > 0:
7            return 'Positive'
8        else:
9            return 'Zero'
10 
11print(signString(-5))
12print(signString(42))
13print(signString(0))
console
'Negative' 'Positive' 'Zero'
Reading The Comment

By the time we get there

The comment # n is either positive or zero isn't decoration. It records what we can safely assume once we've reached that line: the outer test already ruled out negative values.

Each level of nesting narrows the possibilities. The outer if splits the world into "negative" and "everything else". The inner if then splits "everything else" into "positive" and "zero". Nesting is how you narrow down step by step.
MCQ

Two levels of nesting

main.py
def f(x, y):
    if x:
        if y:
            return 'a'
        else:
            return 'b'
    else:
        return 'c'

What does f(True, False) return?

  • A 'a'
  • B 'b'
  • C 'c'
  • D None
Recap

Nested conditionals

  • An if can live inside the body of another if or else, letting you split cases into finer and finer pieces.
  • Once you're inside a nested body, you can safely assume the outer test's result, that's why it's worth commenting.
1.4.4 if-elif-else

Flattening the nesting

Nesting an if inside every else is so common that Python gives it a shortcut: elif, short for "else if". It reads top to bottom instead of drifting rightward.

How It's Checked

First True wins, then it stops

  • Python checks the if, then each elif, in order.
  • As soon as one condition is True, Python runs that body and skips every remaining test.
  • If none of them are True, Python runs the else body, if there is one.
main.py
if TEST1:
    BODY1
elif TEST2:
    BODY2
else:
    BODY3
Try This

The same three cases

This is exactly the nested version from before, rewritten with elif. Same behavior, no rightward drift.

main.py
1def signString(n):
2    if n < 0:
3        return 'Negative'
4    elif n > 0:
5        return 'Positive'
6    else:
7        return 'Zero'
8 
9print(signString(-5))
10print(signString(42))
11print(signString(0))
console
'Negative' 'Positive' 'Zero'
Using Multiple elifs

Zero or more elifs

You can chain as many elifs as you need. Python still checks them strictly in order and stops at the first match.

main.py
1def signString(n):
2    if n < -1000:
3        return 'Very negative'
4    elif n < 0:
5        return 'A bit negative'
6    elif n > 1000:
7        return 'Very positive'
8    elif n > 0:
9        return 'A bit positive'
10    else:
11        return 'Zero'
12 
13print(signString(-1234))
14print(signString(-12))
15print(signString(1234))
16print(signString(12))
17print(signString(0))
console
'Very negative' 'A bit negative' 'Very positive' 'A bit positive' 'Zero'
MCQ

else is optional

main.py
def signString(n):
    if n < 0:
        return 'Negative'
    elif n > 0:
        return 'Positive'

print(signString(0))

There's no else. What happens for signString(0), where neither test is True?

  • A It crashes with a SyntaxError, a chain must end with else.
  • B Python silently falls through the whole chain, and the function returns None.
  • C Python re-checks the if test one more time before giving up.
  • D It prints an empty string.
Activity

Trace the Branch

letterGrade(score) is defined on the next slide, with one if, two elifs, and an else. For each call on the next slide, work out which single branch actually runs, drag (or click, then click the box) its return value into place. As you go, the branch you just traced lights up in the code, so you can see the execution path that call took. Each new correct answer moves the highlight to its own branch.

Activity · Trace the Branch
main.py

All four calls traced. Each call took exactly one path through the if-elif-else chain, and Python stopped checking the moment it found a match. Only the most recent branch stays highlighted, since only one path runs per call.

Recap

if-elif-else

  • Python checks each test in order and runs only the body of the first one that's True, skipping the rest.
  • You can chain zero or more elifs after an if; the trailing else is optional.
  • Without an else, if every test is False, Python falls through silently, no crash, just nothing runs.
1.4.5 if-else Expressions

A conditional that's a value

An if-else statement runs one of two bodies. An if-else expression instead evaluates to one of two values, in a single line.

The General Form

The order flips

Look closely at the order: the value comes first, then the test, then the fallback value. That's the opposite order from an if-else statement, which can feel backwards at first.

main.py
TRUE_VALUE if TEST else FALSE_VALUE
Try This

Five lines become one

This if-else expression is exactly equivalent to the if-else statement we just saw. Use expressions like this sparingly, only when they clearly make code easier to read.

main.py
1def signString(n):
2    return 'Negative' if n < 0 else 'Non-negative'
3 
4print(signString(-5))
5print(signString(42))
6print(signString(0))
console
'Negative' 'Non-negative' 'Non-negative'
MCQ

Evaluate the expression directly

main.py
x = 5
print('a' if x == 0 else 'b')

What does this print?

  • A None
  • B b
  • C the literal text a if x == 0 else b
  • D a
Recap

if-else expressions

  • The general form is TRUE_VALUE if TEST else FALSE_VALUE, note the value comes before the test.
  • Unlike a statement, an if-else expression evaluates to a single value that you can use anywhere a value is expected.
  • Use these sparingly, only when they genuinely make a line easier to read.
1.4.6 Short-Circuit Evaluation

Stopping as soon as the answer is known

Conditions using and and or don't always evaluate both sides. Python short-circuits: it stops as soon as it knows the overall answer, and this can be surprisingly useful.

FRQ · Predict First

What do you think happens?

Before you run it, predict: does this code crash? If not, what does it print?

main.py
print((1 == 2/0) and (3 == 4))
See For Yourself

Run this. It will crash.

Python evaluates the left side of and first. 1 == 2/0 divides by zero before the right side is ever considered. Once you've seen it crash, edit the code to swap the two sides of and and run it again.

main.py
print((1 == 2/0) and (3 == 4))  # run this, it crashes. Then swap the sides and run again.
Now Reverse The Order

Same values swapped: no crash

main.py
print((1 == 2/0) and (3 == 4))  # crashes!
print((3 == 4) and (1 == 2/0))  # prints False, no crash!
Step 1
Python checks (3 == 4), which is False.
Step 2
False and anything is always False, no matter what the right side is.
Step 3
So Python never evaluates 1 == 2/0. It short-circuits straight to False.
The or Operator

Short-circuits to True

or works the same way, just aimed at the opposite outcome. As soon as Python finds a True (or even just truthy) left side, it stops, since True or anything is always True.

main.py
print((1 < 2) or (3 < 4/0))  # True, no crash
print((1 > 2) or (3 > 4/0))  # crashes!
Why This Matters

Guarding against a crash

Say score is usually a number, but is sometimes None. Testing score > 100 directly would crash whenever score is None.

Crashes sometimes: if score > 100:
Always safe: if (score != None) and (score > 100):

When score is None, score != None is False, and and short-circuits before ever comparing None > 100.

MCQ

Crash, or short-circuit?

What does (3 > 4) and (5 == 1/0) evaluate to?

  • A It crashes.
  • B None
  • C True
  • D False

What does (3 < 4) and (5 == 1/0) evaluate to?

  • A It crashes.
  • B False
  • C True
  • D None
Recap

Short-circuit evaluation

  • and stops as soon as it sees a False left side; the right side is never evaluated.
  • or stops as soon as it sees a True (or truthy) left side.
  • This lets a guard like (x != None) and (x > 100) avoid a crash, but use short-circuiting sparingly, only when it makes code clearer.
1.4.7 Truth Tables & De Morgan's Laws

Every possible combination

A Boolean expression is an expression that always evaluates to True or False, no matter what values go in. Once more than one or two variables are involved, it can be easy to lose track of every possible case by eye. A truth table solves that: it lists every combination of inputs in its own row, with a final column showing what the expression evaluates to for that combination of inputs.

A First Table

The table for (A or B)

With two variables, there are four possible combinations. Let's step through building each row.

In row 1:
A is True and B is True, so (A or B) is (True or True), which is True.

In row 2:
A is True and B is False, so (A or B) is (True or False), which is True. Only one side needs to be True.

In row 3:
A is False and B is True, so (A or B) is (False or True), which is True.

In row 4:
A is False and B is False, so (A or B) is (False or False), the only case where or is False.

AB(A or B)
True True True
True False True
False True True
False False False
Helper Columns

Build up to the full expression

For a more complex expression, add a column for each subexpression first. Here, not B is computed before the final column, which keeps the last step simple and less error-prone.

ABnot B(A and (not B))
True True False False
True False True True
False True False False
False False True False
De Morgan's Laws

Two expressions, one table

If the last column of two truth tables matches exactly, the expressions are equivalent, and you can safely swap one for the other. The mathematician Augustus De Morgan proved two especially useful equivalences:

Law 1
not (A or B) ≡ (not A) and (not B)
Law 2
not (A and B) ≡ (not A) or (not B)
The pattern: pushing a not through an or/and flips it to the other operator, and negates each side.
Using It In Practice

Simplify a tangled condition

A restaurant is closed after noon and all day on Monday. (hour > 12) or (day == 'Monday'). To check whether it is open, you need the opposite of that, the not of the whole thing, and that's the tangled condition shown.

De Morgan's first law lets you push the not inward instead of leaving it wrapped around everything: or becomes and, and each side gets its own not.

Each negated comparison simplifies on its own.

tangled· hard to read at a glance
not ((hour > 12) or (day == 'Monday'))
Law 1 applied· push the not inward
(not (hour > 12)) and (not (day == 'Monday'))
simplified· simplify each negated comparison
(hour <= 12) and (day != 'Monday')
MCQ

Spot the imposter

Which of the following is NOT one of De Morgan's Laws?

  • A (not (A and B)) is equivalent to ((not A) or (not B))
  • B (A and B) is equivalent to ((not A) or (not B))
  • C (not (A or B)) is equivalent to ((not A) and (not B))
  • D None of these, they are all De Morgan's Laws.
Activity

True or False?

On the next slide, each chip is a Boolean expression, some built straight from True/False, some as De Morgan pairs of each other. Drag (or click, then click the bin) each one into True or False, based on what it actually evaluates to.

Activity · True or False?

All 8 expressions sorted. Notice the De Morgan pairs, like not (True or False) and (not True) and (not False), always land in the same bin.

Recap

Truth tables & De Morgan's Laws

  • A truth table lists every combination of inputs and the expression's value in each case; matching last columns mean the expressions are equivalent.
  • Helper columns for subexpressions make the final column easier and less error-prone to fill in.
  • not (A or B)(not A) and (not B), and not (A and B)(not A) or (not B).
1.4.8 Improper Use of Conditionals

Ways to misuse what you just learned

Conditionals are powerful enough that Python lets you write them in some genuinely bad ways. Here are the common traps, so you recognize them in other people's code, and avoid them in your own.

Trap 1

Using if instead of elif/else

A common newcomer mistake: writing a second, independent if when a single if-elif or if-else was the better choice. They may run identically, but they are not equally good code.

the right way
if n < 0:
    return 'Negative'
else:
    return 'Non-negative'
the wrong way
if n < 0:
    return 'Negative'
if n >= 0:
    return 'Non-negative'
Why It Matters

Three reasons else wins

  • Easier to read. With else, it's obvious every value is covered. With two separate ifs, you have to reason through both tests yourself.
  • Fewer bugs. Clearer code is easier to reason about, and bugs hide in code you don't fully understand.
  • Faster. The if-else version runs one test. The two-if version always runs both, even though the second is redundant.
two ifs can even change the answer
n = 5
if n > 0:
    n = n - 10
if n < 0:
    print('Negative')  # this runs too!

n starts at 5, positive, so the first body runs and changes n to -5. The second if then tests that new value, finds it negative, and runs too. So this doesn't just run a redundant extra test, it can silently produce a completely different result than an if-elif chain would, which only ever checks the original value once.

Trap 2

Nesting if-else expressions

You can chain if-else expressions together. It "works", but it's confusing and hard to read, which makes it easy to introduce bugs.

avoid this
return 'Negative' if n < 0 else 'Positive' if n > 0 else 'Zero'
Only use if-else expressions in the simplest cases, and never nest them. For anything with real complexity, including nested logic, reach for an if-else or if-elif-else statement instead.
Trap 3 · See For Yourself

Booleans sneaking into arithmetic

Both versions print the same score. Python automatically converts True to 1 and False to 0 in arithmetic, so (strength > 20) * 10 silently "works". Run it and compare.

The Takeaway

It works. Don't do it.

int(True) is 1 and int(False) is 0. You can even drop the explicit int() call, since Python converts booleans for you inside arithmetic.

This conceals the conditional entirely. A reader scanning score = 50 + (strength > 20) * 10 has to notice the boolean-arithmetic trick just to realize there's branching logic here at all. Write the if statement instead.
Trap 4 · See For Yourself

Strings inside or

'' is falsy, so or short-circuits past it and returns 'abc'. This is the same truthy/falsy conversion from bool(), just applied inside and/or instead of an explicit if.

Three Ways To Write The Same Fix

From clever to clear

Not advised: name = name or 'Unknown', a non-Boolean hiding inside or.
Better: name = 'Unknown' if name == '' else name, an honest Boolean test.
Best: if name == '': name = 'Unknown', the clearest of the three, and it never reassigns name to itself.
MCQ

Where's the improper use?

main.py
def testSign(n):
    if n < 0:
        return 'Negative'
    if n > 0:
        return 'Non-negative'

This also has a subtler bug than style: what does it return for n == 0? But first, spot the improper use of conditionals.

  • A The second if should be an elif or else, the two tests are meant to be mutually exclusive alternatives.
  • B The function is missing a return statement.
  • C n < 0 should be n <= 0.
  • D There is no bug, this is perfectly fine style.
Recap

Improper use of conditionals

  • Don't use a second if when elif or else says the same thing more clearly and runs faster.
  • Don't nest if-else expressions; use an if-elif-else statement once things get complex.
  • Don't fold Booleans into arithmetic, and don't use non-Booleans directly inside and/or. Both "work" but conceal the conditional from readers.
Unit Recap

Conditionals: the big picture

  • if / if-else runs a body only when a test is True, or picks between two bodies based on it.
  • Nesting and elif both handle more than two cases; elif is usually the cleaner, flatter choice.
  • if-else expressions evaluate to a value in one line, used sparingly and never nested.
  • Short-circuit evaluation lets and/or skip work, and even guard against crashes.
  • Truth tables and De Morgan's Laws let you prove two Boolean expressions are equivalent, and simplify tangled ones.
  • Every one of these has a proper and an improper form. Tracing the execution path, which branch actually runs for a given input, is how you tell the difference.