Conditionals and loops read the same in Python as in the language you know. Recovery from failure is the part that reads differently.
In C++ and Java the careful move is to test before you act:
check that the string parses, that the file is there, that the
key exists. Python goes the other way. Run the operation and
catch the exception when it fails. The test and the operation
can disagree, and the world can change between them. Exceptions
are ordinary control flow here, not a last resort. This chapter
covers them along with conditionals, placeholders, loops,
pattern matching, the with statement, and
comprehensions.
Python’s comparison operators chain the way they do in mathematics:
# chaining.py
x = 5
print(0 < x < 10) # Chained comparison
#: True
grade = "ok" if x >= 3 else "low" # Conditional expression
print(grade)
#: okThe example also shows a conditional expression: a
one-line if/else that produces a
value.
Adding elif to an if statement
tests several conditions in order:
# if_elif.py
def classify(n):
if n < 0:
return "negative"
elif n == 0:
return "zero"
else:
return "positive"
print(classify(-3), classify(0), classify(7))
#: negative zero positivepass and
...The pass statement does nothing. Use it where
Python’s syntax requires a statement but you have none to run
yet.
... (the Ellipsis literal) is a second
placeholder. On its own as a statement it does nothing, the same
as pass:
# placeholders.py
def not_implemented():
pass # Fill in later
def not_implemented_yet():
...
print(not_implemented(), not_implemented_yet())
#: None Nonepass marks an indented block with nothing in it
yet. ... is the conventional body for a stub whose
implementation lives elsewhere. You normally write it on the
same line as the signature it stubs, as in a Protocol
method.
A while loop runs until its condition is
false:
# while_loop.py
def collatz_sequence(n):
steps = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
print(n)
steps += 1
return steps
print(collatz_sequence(10), "steps")
#: 5
#: 16
#: 8
#: 4
#: 2
#: 1
#: 6 stepsbreak leaves a loop and continue
skips to the next iteration:
# break_continue.py
for n in range(10):
if n == 3:
continue # Skip the rest of this iteration
if n == 6:
break # Leave the loop
print(n, end=" ")
print() # The newline that end=" " left off
#: 0 1 2 4 5The loop prints 0 1 2, skips 3 with
continue, prints 4 5, then stops at
6 with break, so 6
through 9 never print.
break and continue both apply to
the innermost enclosing loop. Python has no labeled
break, so leaving two loops at once means either a
flag, a return from a function that holds both
loops, or the loop else technique that nested_break.py shows
below.
print() ends with a newline by default.
end=" " replaces that newline with a space, so the
numbers print on one line, and a bare print() emits
the missing newline afterward.
Python has no do/while statement.
When the test belongs in the middle or at the bottom of the
body, write while True: and break
out:
# while_true.py
values = [3, 5, 0, 7]
total = 0
while True:
value = values.pop(0)
if value == 0:
break
total += value
print(total)
#: 8The loop must fetch a value before it can test it, so the
test sits between the pop() and the addition.
else
ClauseA loop may have an else clause. It runs when the
loop finishes with no break, and that makes it
natural for search loops:
# loop_else.py
def find_factor(n):
for d in range(2, n):
if n % d == 0:
print(f"{n} = {d} * {n // d}")
break
else:
# No break means no factor found
print(f"{n} is prime")
find_factor(15)
#: 15 = 3 * 5
find_factor(13)
#: 13 is primeThe else belongs to the for, not
the if. A while loop can use
else the same way.
The loop else is also how you leave two nested
loops at once. Put continue in the inner loop’s
else and a break right after it. When
the inner loop breaks, Python skips its
else and the outer break runs. When
the inner loop runs to the end, the continue moves
the outer loop along instead:
# nested_break.py
grid = [[1, 2], [3, 4]]
def locate(target):
for row in grid:
for cell in row:
if cell == target:
print(f"found {cell}")
break # [1]
else: # [2]
continue
break # [3]
else: # [4]
print("not found")
locate(3)
#: found 3
locate(9)
#: not foundlocate(3) walks the first row to the end, so the
inner else ([2]) runs its
continue and the outer loop moves on to the second
row. There the 3 matches, the inner
break ([1]) skips the
else, and the outer break
([3]) runs right after. locate(9)
breaks neither loop, so the inner else continues on
each row and the outer else ([4])
prints "not found".
locate() is a function so the listing can run
two searches. Inside a function, a return at the
match leaves both loops in one step. The else form
serves loops at the top level of a module, and a function with
more work to do after the search.
range(),
enumerate(), and zip()for walks the items of any iterable. A list, a
set, a dictionary, or a string needs no index. Use
range() for counting and enumerate()
when you also need the index:
# looping.py
for i in range(3):
print(i, end=" ")
print()
#: 0 1 2
names = ["Alice", "Bob", "Carol", "Ted"]
for index, name in enumerate(names):
print(index, name)
#: 0 Alice
#: 1 Bob
#: 2 Carol
#: 3 Ted
for n, name in enumerate(names, start=1):
print(n, name)
#: 1 Alice
#: 2 Bob
#: 3 Carol
#: 4 Tedenumerate() yields (index, item)
pairs counting from zero, and the loop here unpacks each pair
into index and name.
for i in range(len(names)): with
names[i] inside does the same job, but that form
names the index and not the item, so every line that needs the
item repeats the names[i] lookup.
enumerate() hands you both. For output that people
read, which usually counts from one, start=1 begins
the index at one, so the loop body adds nothing to it.
zip() walks several sequences at once:
# zipping.py
names = ["Alice", "Bob", "Carol", "Ted"]
scores = [88, 91, 79, 54, 99] # One score too many
for name, score in zip(names, scores):
print(name, score)
#: Alice 88
#: Bob 91
#: Carol 79
#: Ted 54
try:
list(zip(names, scores, strict=True))
except ValueError as e:
print(e)
#: zip() argument 2 is longer than argument 1zip() produces one item from each sequence and
stops when the shortest runs out, so it drops the extra score.
Stopping without an error is convenient when the lengths differ
on purpose and a bug when you expect them to match.
strict=True raises a ValueError on the
mismatch instead.
When you need the index as well, wrap the zip()
in enumerate(). The nesting shows up in the loop
header, where the inner pair needs parentheses:
for i, (name, score) in enumerate(zip(names, scores)):.
The walrus operator := assigns a value
as part of an expression, so you can compute, name, and test a
value in one place:
# walrus.py
text = "hello"
# Without it, you assign first and then test:
length = len(text)
if length > 3:
print(f"{length} characters")
#: 5 characters
# The walrus assigns inside the condition:
if (n := len(text)) > 3:
print(f"{n} characters")
#: 5 characters
stack = ["a", "b", "c"]
while stack and (item := stack.pop()) != "a":
print("processing", item)
#: processing c
#: processing bEach walrus expression here needs its parentheses.
:= binds more loosely than a comparison, so
if n := len(text) > 3: gives n the
result of the comparison, True, and not the
length.
The while loop is where the walrus helps most.
The header pops a value, names it, and tests it, so the body
starts with that value in hand. The walrus also collapses while_true.py into its
loop header: while (value := values.pop(0)) != 0:.
A comprehension can use := the same way, and Comprehensions
covers that use.
Changing a container while a for loop walks it
is the classic control-flow bug. Containers
shows a list skipping items as the loop removes
them, and a
dict raising a RuntimeError under
the same treatment. Lists and dictionaries are the two
containers you are most likely to mutate this way, and each one
fails differently. A list
comprehension fixes the list case by building a new,
filtered list instead of mutating the original in place:
# mutating_while_looping.py
scores = [1, 2, 2, 3]
for s in scores:
if s == 2:
scores.remove(s)
print(scores)
#: [1, 2, 3]
print([s for s in [1, 2, 2, 3] if s != 2])
#: [1, 3]
ages = {"a": 1, "b": 2}
try:
for name in ages:
ages[name + "!"] = 0
except RuntimeError as e:
print(e)
#: dictionary changed size during iterationThe list loop walks by position. Removing an item shifts the
next one down into the slot the loop has passed, so the loop
skips it and one of the two 2s survives, with no
exception to tell you. The dictionary raises a
RuntimeError instead of skipping silently. The fix
is the same for both. Build a new container with a
comprehension, or collect what to remove first and remove it
after the loop.
The match statement compares a value against
structural patterns. It resembles a C switch, but a
pattern can look inside a value and pull out its parts:
# pattern_matching.py
def run(command):
match command.split():
case ["go", direction]:
return f"moving {direction}"
case ["quit"]:
return "goodbye"
case _: # Default
return "unknown command"
print(run("go north"))
#: moving north
print(run("quit"))
#: goodbye
print(run("dance"))
#: unknown commandOnly the first matching case runs. Unlike C, a
case does not fall through, so it needs no
break.
The first case destructures the split command.
It matches a two-item list starting with "go" and
binds the second item to direction. A bare name in
a case captures rather than compares.
case direction: binds anything to
direction and matches every value. Write a constant
as a literal (case "quit":) or as a dotted name
(case Command.QUIT:).
match and case are soft
keywords: they are keywords only in this statement, so
existing code that uses match as a variable name
still runs. Avoid the name, though, because a reader must work
out which meaning applies. Pattern Matching
covers match in detail.
Python signals an error by raising an exception. As
in C++ and Java, an exception propagates up the call stack until
it finds a handler. An exception that finds no handler stops the
program and prints the traceback. In Python, a handler is
except followed by the exception type it handles.
You can give only the type, or add an as to capture
the exception object, as in
except ValueError as e:
# demonstrate_exceptions.py
def parse_int(text):
try:
return int(text)
except ValueError:
return None
print(parse_int("42"))
#: 42
print(parse_int("oops"))
#: None
def checked_divide(a, b):
if b == 0:
raise ValueError("Divide by zero")
return a / b
def divide_and_report(a, b):
try:
checked_divide(a, b)
except ValueError as e:
print("caught:", e)
else:
print("no exception")
finally:
print("finally always runs")
divide_and_report(1, 0)
#: caught: Divide by zero
#: finally always runs
divide_and_report(1, 1)
#: no exception
#: finally always runschecked_divide() raises a
ValueError rather than letting Python’s own
ZeroDivisionError through. Raise your own exception
that way when the caller should hear about the bad argument
rather than the failed arithmetic.
The optional else runs when the try
block raises no exception, the same shape as the loop
else that runs when the loop hits no
break. Code in the else is outside the
reach of the except clauses, so no
except clause mistakes an exception from the
follow-up work for the failure it expects.
The optional finally always runs, and that makes
it the place for cleanup. A return,
break, or continue inside
finally swallows any exception in flight, so keep
all three out of cleanup code:
# finally_swallows.py
def risky():
try:
raise ValueError("boom")
finally:
return "swallowed"
print(risky())
#: swallowedrisky() raises a ValueError, but
the return in finally discards it
before it reaches the caller, so the caller sees
"swallowed" and no trace of the exception. Python
also flags the return in finally at
compile time. Running the listing prints
SyntaxWarning: 'return' in a 'finally' block to
standard error before swallowed.
Catch an exception only when you can do something about it. A
bare except: with no type catches everything,
including the KeyboardInterrupt you press to stop a
runaway program. It also catches a bug in the try
block and makes it look like an expected failure.
except Exception: is the broad catch you want
instead. KeyboardInterrupt and
SystemExit derive from BaseException
rather than Exception, so they propagate past that
clause and still stop the program.
To handle several types the same way, give a tuple:
except (ValueError, TypeError) as e:. Python tries
the except clauses in order and runs the first
whose type matches, so a broad clause above a narrow one makes
the narrow one unreachable. Order them most specific first.
To log an exception and still let it propagate, re-raise it
with a bare raise.
Raising an exception while handling another attaches the
handled exception to the new one. Python reports both, and
from decides how the two connect:
| Form | What Python prints above the new exception |
|---|---|
raise X |
During handling of the above exception, another exception occurred: |
raise X from e |
The above exception was the direct cause of the following exception: |
raise X from None |
Nothing: only X appears |
All three raise the same exception and differ only in that report:
# exception_chaining.py
import textwrap
import traceback
class BadNumber(Exception):
pass
def implicit(text):
try:
return int(text)
except ValueError:
raise BadNumber(text)
def explicit(text):
try:
return int(text)
except ValueError as e:
raise BadNumber(text) from e
def suppressed(text):
try:
return int(text)
except ValueError:
raise BadNumber(text) from None
def joining_line(e):
for part in traceback.format_exception(e):
line = part.strip()
if (line.endswith("exception occurred:")
or line.endswith("following exception:")):
return line
return "nothing shown above it"
for parse in (implicit, explicit, suppressed):
try:
parse("seven")
except BadNumber as e:
print(f"{parse.__name__}:")
for chunk in textwrap.wrap(joining_line(e), 55):
print(" ", chunk)
#: implicit:
#: During handling of the above exception, another
#: exception occurred:
#: explicit:
#: The above exception was the direct cause of the
#: following exception:
#: suppressed:
#: nothing shown above itBadNumber is a custom exception type: a class
derived from Exception. Its body is
pass because it needs no behavior of its own. The
handler matches on the class. Classes covers class
definitions in full.
joining_line() digs the joining sentence out of
the formatted traceback, so the output above is the text Python
prints, not a summary of it.
from e sets __cause__ and produces
the “direct cause” line. With no from, Python still
records the earlier exception in __context__ and
produces the “During handling” line. from None sets
__suppress_context__, and nothing appears above the
new exception.
Use from e when the earlier exception explains
this one, and from None when the earlier exception
distracts from your own message.
Python’s culture leans on “easier to ask forgiveness than permission,” abbreviated EAFP. Try the operation and handle the exception, rather than checking every precondition first. The opposite style, “look before you leap” (LBYL), tests first, and it breaks whenever the test and the operation disagree:
# eafp.py
def careful(text):
if text.isdigit():
return int(text)
return None
def forgiving(text):
try:
return int(text)
except ValueError:
return None
print(careful("-5"), forgiving("-5"))
#: None -5
try:
careful("\N{SUPERSCRIPT TWO}")
except ValueError as e:
print("careful:", e)
#: careful: invalid literal for int() with base 10: '²'
print(forgiving("\N{SUPERSCRIPT TWO}"))
#: Noneisdigit() and int() disagree in
both directions. isdigit() rejects
"-5", which int() converts fine, and
isdigit() accepts "²", which
int() refuses. The try block asks the
only question that matters: does this conversion work? The world
can also change between the test and the operation. A file that
exists at the if can disappear before the
open(), and only the EAFP form is safe against
that.
A with block guarantees that setup and cleanup
run as a pair, even if the body raises an exception. It does the
work of a try/finally whose
finally holds the cleanup. The object that
with manages supplies the cleanup, so you cannot
leave it out. Opening a file is the canonical case. The
with block always closes the file on exit:
# context_manager.py
import tempfile
from pathlib import Path
path = Path(tempfile.gettempdir()) / "demo.txt"
with path.open("w") as f:
f.write("one\ntwo\n") # Automatic f.close()
with path.open() as f:
for line in f:
print(line.strip())
#: one
#: two
try:
with path.open("w") as f:
f.write("partial")
raise RuntimeError("failed midway")
except RuntimeError as e:
print(e)
#: failed midway
print("closed:", f.closed)
#: closed: True
path.unlink() # Delete the fileThe exception propagates, but the with closes
the file first. A with statement creates a
guarantee about the exit, not a scope, so f is
still in scope afterward and the listing can print
f.closed.
Closing the file is cleanup that runs whether or not the block succeeds. Cleanup contrasts that guarantee with leaving the close to Python’s garbage collector.
Anything that acquires a resource (a file, a lock, a network
connection) can be a context manager. An object becomes one by
defining two methods, __enter__() and
__exit__(): with calls
__enter__() at the top of the block and
__exit__() as control leaves it, and the cleanup
lives in __exit__(). Context Managers
shows how to write your own.
For reading or writing a whole file in one call,
pathlib provides read_text() and
write_text(), which open the file and close it
again.
A comprehension builds a list, dictionary, or set from an iterable in one expression, replacing a loop that builds up a result:
# comprehensions_intro.py
squares = [n * n for n in range(5)] # List comprehension
print(squares)
#: [0, 1, 4, 9, 16]
# With a filter
evens = [n for n in range(10) if n % 2 == 0]
print(evens)
#: [0, 2, 4, 6, 8]
# Dict comprehension
lengths = {w: len(w) for w in ["a", "bb"]}
print(lengths)
#: {'a': 1, 'bb': 2}
parities = {n % 2 for n in range(10)} # Set comprehension
print(parities)
#: {0, 1}Every form has the same order: the expression that produces
each item, the for clause that supplies the values,
and an optional if that filters them. The
delimiters choose the container. Brackets build a list, braces
build a set, and braces around a key: value
expression build a dictionary. Comprehensions
covers nesting, generator expressions, and when to write a loop
instead.
This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.
loop_else.py, call
find_factor(97). Predict whether the
for loop’s else clause runs before you
check, then confirm.collatz_sequence() in while_loop.py to also
count how many times n is odd, and print that count
alongside the step count.break_continue.py, swap
the order of the two if blocks, so the
n == 6 break check comes first and the
n == 3 continue check comes second.
Predict whether the output changes before running it, then
explain what you find.demonstrate_exceptions.py,
add a call divide_and_report(1, "x") (a
TypeError that except ValueError does
not catch). Run it and read the traceback that escapes.pattern_matching.py, add a
case ["go", direction, distance] that reports both
parts, and check what run("go north 3") returns
before and after you add it.evens list comprehension in comprehensions_intro.py as
a for loop that appends to a list, then say which
version you would rather read six months from now.exception_chaining.py, add
a fourth function that catches the ValueError and
raises BadNumber from a different
exception object it constructs. Predict which line
joining_line() prints before you run it.context_manager.py’s
reading half using path.read_text(). Say what the
with form gives you that the one-liner does not,
and when that matters.mutating_while_looping.py,
change the list to [2, 2, 1, 3], so a
2 sits in the first slot. Use the shifting-slots
explanation to predict what the loop leaves in
scores, then run it to check.