Python From Scratch — Part 3: Booleans & Control Flow
Every `if` you write rests on a value that's either `True` or `False` — but `==` and `is` are not the same question, and the day you confuse them on the number 257 is the day you find out why. Booleans, truthiness, and Python's whitespace-is-the-syntax rule, up close.
Part 3 of 23 · Level: Beginner · From Hello World to Neural Networks
Every interesting program eventually has to make a decision: is the password right, is the cart empty, did the user actually type a number? Decisions are built out of one tiny, opinionated thing — a value that is either True or False. Get comfortable with that, and the branching almost writes itself.
Booleans: the two-value type
A boolean is Python's smallest type: it holds exactly one of two values, True or False. Mind the capital letters — true is just an undefined name, and Python will complain.
is_raining = True
has_umbrella = False
print(is_raining) # => True
print(type(is_raining)) # => <class 'bool'>
You rarely type True/False by hand, though. Usually you get them by asking a question about your data — which is what comparisons are for.
Comparison operators
A comparison takes two values and hands you back a boolean:
print(3 < 5) # => True
print(3 > 5) # => False
print(10 == 10) # => True (equal?)
print(10 != 10) # => False (not equal?)
print(7 >= 7) # => True
print(7 <= 6) # => False
The one that trips up newcomers from other languages: == is comparison ("are these equal?"), while a single = is assignment ("make this variable hold that value"). Python actually protects you — writing if x = 5: is a syntax error, not a silent bug. Small mercy, gratefully accepted.
Comparisons work on strings too, ordered by Unicode code point (which is roughly alphabetical):
print("apple" < "banana") # => True
print("Zoo" < "apple") # => True (uppercase sorts before lowercase)
And Python lets you chain them, the way maths does:
age = 25
print(18 <= age < 65) # => True
That reads as "18 is less than or equal to age, and age is less than 65" — no need to spell out the and yourself.
== vs is: the gotcha nobody warns you about
Here is the one that burns people. == asks "are these two values equal?" is asks "are these two the exact same object in memory?" They sound interchangeable right up until they aren't.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # => True (same contents)
print(a is b) # => False (two different lists that happen to match)
a and b hold the same numbers, so == is happy. But they are two separate lists at two different addresses, so is says no.
Now the part that makes is genuinely treacherous for beginners: Python caches small integers (-5 to 256), so equal values in that range secretly share one object — and equal values outside it don't.
a = 256
b = int("256") # built at runtime, so not a literal
print(a == b) # => True
print(a is b) # => True (256 is cached — one shared object)
x = 257
y = int("257")
print(x == y) # => True
print(x is y) # => False (257 isn't cached — two objects, same value)
If you ever "tested" identity with is on small numbers and it seemed to work, you were riding the cache — and it will betray you the moment a value crosses 256. Rule of thumb: use == to compare values; reserve is for identity — which in practice means comparing against the singletons None, True, and False.
result = None
if result is None: # correct, idiomatic
print("no result yet")
if result == None: # works, but not the done thing
print("please don't")
is None is the blessed way to check for None: it is faster, it can't be fooled by a class that redefines ==, and every Python reviewer expects it.
and, or, not
To combine booleans, Python uses actual English words instead of && and ||:
logged_in = True
is_admin = False
print(logged_in and is_admin) # => False (needs both)
print(logged_in or is_admin) # => True (needs either)
print(not is_admin) # => True (flips it)
andisTrueonly if both sides are true.orisTrueif at least one side is true.notflips a boolean.
One detail worth learning early: these short-circuit. and stops the instant it hits something false; or stops the instant it hits something true — because the answer is already decided. That is not just a speed trick, it is a safety one:
name = ""
# name is empty (falsy), so Python never evaluates name[0],
# and this doesn't crash:
if name and name[0] == "A":
print("starts with A")
else:
print("no name, no crash")
Output:
no name, no crash
Put your cheap or protective check on the left, and you can safely guard the expensive or risky one on the right.
if / elif / else, and why indentation is the syntax
Now we spend those booleans. An if statement runs a block only when its condition is truthy:
temperature = 30
if temperature > 25:
print("Warm out.")
print("Wear shorts.")
elif temperature > 10:
print("Bring a jacket.")
else:
print("It's freezing.")
Output:
Warm out.
Wear shorts.
Read it top to bottom: Python checks each condition in order and runs the first block that is true, then skips the rest. elif ("else if") chains alternatives; else catches everything left over. Both are optional — a lone if is perfectly fine.
Here is the thing that makes Python Python: the indentation is the syntax. Most languages use { } to mark a block and treat whitespace as decoration. Python has no braces — the spaces in front of those print lines are what tell the interpreter "these belong to the if." Change the indentation and you change the meaning:
if False:
print("skipped")
print("always runs") # not indented, so not part of the if
Output:
always runs
Practical rules: use four spaces per level (the whole community does), stay consistent, and never mix tabs and spaces — Python will reject the file with a TabError. Your editor almost certainly handles this for you; let it.
Truthiness: values that act like booleans
if does not actually demand a True/False. It takes any value and decides whether it is "truthy" or "falsy." The falsy values are a short list worth memorising, because everything else is truthy:
FalseNone0(and0.0)""— the empty string[],{},()— empty list, dict, tuple
cart = []
if cart:
print(f"{len(cart)} items")
else:
print("Your cart is empty.")
Output:
Your cart is empty.
if cart: reads as "if the cart has anything in it." That is cleaner than if len(cart) > 0: and it is the idiomatic way to ask "is this empty?"
But there is a gotcha hiding in plain sight. Truthiness makes 0 and None look identical to an if, and sometimes they mean very different things:
def describe(count):
if not count:
return "nothing to report"
return f"{count} things"
print(describe(0)) # => nothing to report
print(describe(None)) # => nothing to report
If 0 is a legitimate value (a real count of zero) and None means "we don't know yet," collapsing them with if not count: is a bug. When you specifically mean "is this missing," say exactly that: if count is None:.
The conditional expression (Python's ternary)
Sometimes a full if/else is overkill — you just want to pick between two values. Python has a compact one-line form that reads almost like English:
age = 20
label = "adult" if age >= 18 else "minor"
print(label) # => adult
The shape is value_if_true if condition else value_if_false. Note the order: the result comes first, then the condition. It feels backwards for about a day, then you'll reach for it constantly:
n = 7
parity = "even" if n % 2 == 0 else "odd"
print(parity) # => odd
Keep it to genuinely simple choices. If you catch yourself nesting these, that is your cue to go back to a proper if/elif/else — readability wins.
A first look at match / case
Python 3.10 added match, a tidier way to branch on the shape of a value than a tall stack of elifs:
def http_message(status):
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500 | 503: # match several values with |
return "Server error"
case _: # _ is the catch-all, like else
return "Something else"
print(http_message(404)) # => Not Found
print(http_message(503)) # => Server error
print(http_message(999)) # => Something else
The _ case is the wildcard — it matches anything you didn't handle, playing the role of else. For a plain value check like this, an if/elif chain would do the same job; match really earns its keep later, when you start destructuring tuples and objects. For now, just know it exists and reads nicely.
Next up
You can now make decisions — but a program that decides just once isn't much of a program. In Part 4: Loops, we put these same booleans to work as conditions that repeat, teaching Python to do the tedious thing ten thousand times without complaint.
Jako Heiberg
Software developer with 40+ years of building things that work. Full-stack, FastAPI, React. Based in Cape Town, working remotely, worldwide.