Making Decisions with Conditions
if, elif and else; comparison versus equality; what Python considers true when the thing is not a boolean; and combining conditions without creating something nobody can read.
if, elif and else; comparison versus equality; what Python considers true when the thing is not a boolean; and combining conditions without creating something nobody can read.
A shop applied a ten percent discount to customers who were entitled to none. Not through fraud, and not through a rule anybody wrote — through four lines of code that could not tell "the caller asked for zero discount" apart from "the caller said nothing at all".
Every program you have written so far runs straight through: line one, line two, line three, done. By the end of this lesson your programs will make choices instead. You will also meet Python's idea of what counts as true, which is broader than you expect and is exactly what caused that discount.
The if statement runs a block of code only when a condition
holds:
photo_count = 400
if photo_count > 100:
print("That is a lot of photos")Two pieces of punctuation matter. The colon at the end of
the if line opens the block. The indentation on the next
line says what is inside it.
Python uses indentation the way other languages use braces — it
is not decoration, it is the syntax. Everything indented under
the if belongs to it; the first line that returns to the
previous level is outside it again:
if photo_count > 100:
print("That is a lot of photos") # only when the test passes
print("Processing in batches") # also only then
print("Done") # alwaysUse four spaces per level. Any consistent amount works, but four is the convention and every editor can be told to insert them when you press Tab.
else covers the case where the test failed:
if photo_count > 100:
print("Processing in batches")
else:
print("Processing all at once")elif — short for "else if" — chains more tests. Python tries
them in order and stops at the first that passes:
if photo_count == 0:
print("Nothing to do")
elif photo_count < 50:
print("Processing all at once")
elif photo_count < 500:
print("Processing in batches")
else:
print("This will take a while")The order matters, and only the first match runs.
photo_count == 0 ?
If yes, print "Nothing to do" and the whole chain is over. If no, carry on.
photo_count < 50 ?
Reaching this line already proves the count is not zero, so the test does not need to say so.
photo_count < 500 ?
And reaching this line proves it is 50 or more. That is why the branch does not need "and 50 or more" written into it.
else — everything left over
No test to write. If none of the above matched, this runs.
The comparison operators:
The most common beginner error in the language is writing =
where == was meant. One equals sign assigns, two compare.
Python catches this and refuses to run rather than doing
something surprising:
Comparisons can be chained, which reads exactly as it looks:
That is genuinely one expression meaning both things, not a trick. Most languages make you write it twice.
Text compares too, alphabetically, and is case-sensitive:
and, or and not join conditions together. Python uses
words rather than symbols, which reads well:
and needs both sides. or needs at least one. not flips a
condition.
There is a useful detail: these short-circuit. and stops
at the first false part, and or stops at the first true one,
without evaluating the rest. That lets you guard something
expensive or unsafe with a cheap check on its left:
If photographer is None, the left side is false, so the
right side never runs — and the crash from asking None for its
.name never happens. Written the other way round, it would.
A condition does not have to be a comparison. Any value can be tested, and Python has rules about which count as true.
These are falsy — they behave as false in a condition:
Everything else is truthy. That includes any non-empty text, any non-zero number, and any collection with something in it.
This lets you write conditions that read pleasantly:
And now the bug this creates, which is worth recognising on sight.
Bad — using truthiness to check whether a value was supplied.
Good — checking for absence, not for emptiness.
A discount of zero is falsy, so the first version cannot tell
"the caller asked for no discount" from "the caller said
nothing", and silently applies ten percent to a customer who was
entitled to none. The same trap catches an empty string that
someone deliberately saved, and a quantity of zero. When you
mean "was this provided?", test is None; keep truthiness for
when you genuinely mean "is this empty?".
Two more tests you will use constantly.
in asks whether something is present:
That second form is much better than three ors chained
together, and adding a fourth extension later is a one-word
change.
is asks whether two names refer to the same object, rather
than to equal values:
This is the label picture from two lessons ago, made into an operator.
Do these hold the same value?
What you mean nearly every time you compare two things.
Two separate lists with identical contents pass this.
Are these the same object?
Whether the two names are tied to one thing in memory.
Reserve it for None, True and False. Using it on
numbers or text appears to work and then fails on a value you
did not test.
Occasionally you need a branch that does nothing. An empty block is a syntax error, so Python has a word for it:
pass is a placeholder meaning "no statement here". Use it when
the empty branch is deliberate — and prefer a comment beside it
saying why, so the next reader knows it is a decision rather
than an unfinished thought.
Your programs can now take different paths depending on what they find. You also know Python's truthiness rules and the specific bug they cause — the zero, or the empty string, that was deliberate and gets overwritten by a default. That one appears in real codebases constantly, and you will now spot it in review.
Next is Lists and Tuples, the first collections. Conditions
get much more useful once you have many things to ask questions
about, and the in test you just met is about to become one of
the operations you use most.
Before you move on, write a small program that asks for a
number and reports something different for negative, zero, small
and large. Then deliberately break it: use = instead of ==
and read the error, and try the falsy trap by treating zero as
"nothing entered". Causing both on purpose, once, is how you
stop causing them by accident.
False None 0 0.0
"" [] {} ()SHAPE
if condition: colon opens the block
do_this() four spaces mark what is inside
elif other_condition: tried in order, first match wins
do_that()
else:
do_the_other()
COMPARING
== != equal, not equal
> < >= <=
50 <= n <= 500 chained, and means both
= is assignment two equals signs to compare
COMBINING
and or not words, not symbols
short-circuit x is not None and x.name == "Ana"
FALSY - everything else is truthy
False None 0 0.0 "" [] {} ()
TESTS
if photos: non-empty
if x is None: was it supplied? <- not truthiness
if x in collection: membership
a == b same value <- use this
a is b same object <- only for None/True/False
PLACEHOLDER
pass a deliberately empty block
THE TRAP
0 and "" are falsy so `if not x` cannot tell
"not given" from "given as empty"
use `is None` for that every timea == b # equal note: TWO equals signs
a != b # not equal
a > b # greater than
a < b # less than
a >= b # greater than or equal
a <= b # less than or equalif photo_count = 400:
# SyntaxError: invalid syntax. Maybe you meant '==' instead of '='?if 50 <= photo_count <= 500:
print("A normal-sized batch")print("apple" < "banana") # True
print("Ana" == "ana") # False - different strings
print("Ana".lower() == "ana") # True - compare like with likeif photo_count > 0 and photographer_is_known:
process_photos()
if file_missing or file_empty:
print("Nothing to read")
if not is_processed:
process_photos()if photographer is not None and photographer.name == "Ana":
...if photos: # not empty
process(photos)
if not name: # empty string or None
print("Please give a name")def apply_discount(price, discount):
if not discount:
discount = 0.10 # "no discount given, use the default"
return price * (1 - discount)
apply_discount(100, 0) # 90.0 - customer wanted NO discountdef apply_discount(price, discount=None):
if discount is None:
discount = 0.10
return price * (1 - discount)
apply_discount(100, 0) # 100.0 - respects the zeroif "lisbon" in title.lower():
tag_as_portugal()
if extension in [".jpg", ".jpeg", ".png"]:
process_as_image()a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True - same contents
print(a is b) # False - two different listsif photo.is_corrupt:
pass # deliberately ignored, for now
else:
process(photo)