Python raise, assert and finally
PCAP objective 2.1 goes past catching into causing. `raise` throws deliberately and a bare `raise` re-throws what you are handling. `assert` is a debugging statement that disappears under `-O` and has a spectacular failure mode when written with brackets. And `finally` runs on every path out of a `try` - including, if you are careless, in a way that discards the exception entirely.
Exceptions in Depth Guide 1 of 25 Intermediate
- Python3.14.4
- OSUbuntu 26.04 LTS
- pip25.1.1
- TimeAbout 18 min
- Reviewed23 August 2026
Written against the versions above. Python 3.14 warns about `return` inside `finally` (PEP 765). The behaviour is unchanged and is still examinable - see {{guide:python-314-versus-the-syllabus}}. `raise ... from ...` has been available since Python 3.0.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RUNNER01 | 192.168.0.27 | Ubuntu 26.04 LTS | Python 3.14.4 - the only machine this path needs | 2 Core | 4 GB | 50 GB |
Before you start
- guide 24 - all four keywords of a
trystatement. - guide 23 - you can only raise something derived from
BaseException.
-
raise, with a message
raise SomeError("message")creates an exception object and throws it. It behaves exactly like one Python raised itself - catchable, with a traceback if nobody catches it.bash Example session cat > ~/py/raisebasic.py <<'PY'def check(n): if n < 0: raise ValueError("n must not be negative, got " + str(n)) return n print(check(1))try: check(-1)except ValueError as e: print("caught:", e)check(-2)PYpython3 ~/py/raisebasic.py1caught: n must not be negative, got -1Traceback (most recent call last): File "/home/sysadmin/py/raisebasic.py", line 12, in <module> check(-2) ~~~~~^^^^ File "/home/sysadmin/py/raisebasic.py", line 3, in check raise ValueError("n must not be negative, got " + str(n))ValueError: n must not be negative, got -2[exit 1]Expected result
1, thencaught: n must not be negative, got -1, then the same exception uncaught as a traceback.Success conditionYou can raise an exception with a useful message.
-
A bare raise re-raises what you are handling
Inside an
exceptblock,raisewith no argument re-throws the exception currently being handled. That is how you act on a failure - log it, clean up - without pretending you fixed it.bash Example session cat > ~/py/reraise.py <<'PY'try: try: 1 / 0 except ZeroDivisionError: print("logging, then re-raising") raiseexcept ZeroDivisionError as e: print("the outer handler got it too:", e)PYpython3 ~/py/reraise.pylogging, then re-raisingthe outer handler got it too: division by zeroExpected result
logging, then re-raising, then the outer handler catching the same exception.Success conditionYou can handle an exception partially and pass it on.
-
A bare raise with nothing to re-raise
Not a
SyntaxError- a runtimeRuntimeError, because whether an exception is being handled is not knowable at compile time.bash Example session cat > ~/py/barenothing.py <<'PY'raisePYpython3 ~/py/barenothing.pyTraceback (most recent call last): File "/home/sysadmin/py/barenothing.py", line 1, in <module> raiseRuntimeError: No active exception to reraise[exit 1]Expected result
RuntimeError: No active exception to reraise.Success conditionYou can name what a stray
raiseproduces. -
raise from, to keep the original cause
When you translate a low-level failure into a domain-specific one,
raise ... from erecords what caused it. The new exception carries the old one on__cause__.bash Example session cat > ~/py/raisefrom.py <<'PY'try: try: int("x") except ValueError as e: raise RuntimeError("could not parse the input") from eexcept RuntimeError as e: print("caught:", e) print("cause:", type(e.__cause__).__name__, "-", e.__cause__)PYpython3 ~/py/raisefrom.pycaught: could not parse the inputcause: ValueError - invalid literal for int() with base 10: 'x'Expected result
caught: could not parse the input, thencause: ValueError - invalid literal for int() with base 10: 'x'.Success conditionYou can chain one exception to another without losing information.
-
assert
assert condition, messageraisesAssertionErrorif the condition is falsy and does nothing at all if it is truthy. It is a debugging statement: a claim about what should already be true.bash Example session cat > ~/py/assertok.py <<'PY'total = 10assert total > 0print("the assertion passed, so we get here")assert total > 100, "total is too small: " + str(total)print("never reached")PYpython3 ~/py/assertok.pythe assertion passed, so we get hereTraceback (most recent call last): File "/home/sysadmin/py/assertok.py", line 4, in <module> assert total > 100, "total is too small: " + str(total) ^^^^^^^^^^^AssertionError: total is too small: 10[exit 1]cat > ~/py/assertbare.py <<'PY'assert 1 == 2PYpython3 ~/py/assertbare.pyTraceback (most recent call last): File "/home/sysadmin/py/assertbare.py", line 1, in <module> assert 1 == 2 ^^^^^^AssertionError[exit 1]Expected resultThe passing assertion, then
AssertionError: total is too small: 10, then a bareAssertionErrorwith no message.Success conditionYou can write an assertion and read the error it produces.
-
assert disappears under -O
This is the fact that makes
assertunsuitable for validating input. The-Oflag removes every assertion from the compiled code.The same file that just raised, run again with one flag added.
bash Example session python3 -O ~/py/assertbare.py && echo "exit 0 - the assertion was removed by -O"exit 0 - the assertion was removed by -OExpected result
exit 0 - the assertion was removed by -O- the assertion did not run at all.Success conditionYou know why
assertmust not be used to check user input. -
The assert that can never fail
Because
assertis a statement taking two comma-separated expressions, adding brackets turns both into a single tuple - and a non-empty tuple is always truthy.bash Example session cat > ~/py/asserttuple.py <<'PY'assert (1 == 2, "this message is part of a tuple")print("no AssertionError was raised")PYpython3 ~/py/asserttuple.py/home/sysadmin/py/asserttuple.py:1: SyntaxWarning: assertion is always true, perhaps remove parentheses? assert (1 == 2, "this message is part of a tuple")no AssertionError was raisedExpected resultA
SyntaxWarningabout the parentheses, thenno AssertionError was raised- despite1 == 2.Success conditionYou can spot an assertion that has been disabled by its own brackets.
-
finally runs before the value is handed back
A
returninsidetrydoes not leave immediately -finallyruns first, and only then does the value reach the caller.bash Example session cat > ~/py/finallywins.py <<'PY'def f(): try: return "from try" finally: print("finally ran before the value was handed back") print(f())PYpython3 ~/py/finallywins.pyfinally ran before the value was handed backfrom tryExpected result
finally ran before the value was handed back, thenfrom try.Success conditionYou can order the output of a function with
returninsidetry. -
finally can discard an exception entirely
If
finallycontains areturn, it wins outright - and anything thetrywas carrying, including an exception, is thrown away.The function below raises
ValueError. It returns a string.bash Example session cat > ~/py/finallyswallow.py <<'PY'def g(): try: raise ValueError("this exception is about to disappear") finally: return "finally swallowed it" print(g())PYpython3 ~/py/finallyswallow.py/home/sysadmin/py/finallyswallow.py:5: SyntaxWarning: 'return' in a 'finally' block return "finally swallowed it"finally swallowed itExpected resultA
SyntaxWarning, thenfinally swallowed it- and no traceback at all.Success conditionYou can explain how an exception can vanish without a handler.
-
What the exam does with this objective
PCAP objective 2.1 is half of a 14% section, and this is where its harder questions are:
"What does this function return?" - with a
returnin bothtryandfinally. Thefinallyone."Does this assertion fail?" - if it has brackets round both parts, no.
"What happens to assertions under
-O?" - they are removed."What does a bare
raisedo?" - re-raises the exception being handled."What does a bare
raisedo outside a handler?" -RuntimeError."When should you use
assertrather thanraise?" - for internal invariants, never for input validation."Does
finallyrun when the exception is unhandled?" - yes, on the way out.guide 2 is next.
bash rm -f ~/py/raisebasic.py ~/py/reraise.py ~/py/barenothing.py ~/py/raisefrom.py ~/py/assertok.py ~/py/assertbare.py ~/py/asserttuple.py ~/py/finallywins.py ~/py/finallyswallow.py && ls -A ~/pyExpected resultAn empty scratch directory.
Success conditionYou can raise, assert and clean up deliberately.
Troubleshooting
An assertion never fires however wrong the condition is.
Why: It was written
assert (cond, "msg")- the brackets make a truthy tuple.Fix:Remove the brackets:
assert cond, "msg". Python emits aSyntaxWarningfor this; do not ignore it.Validation passes in production and fails in testing.
Why: The check is an
assert, and production runs with-OorPYTHONOPTIMIZEset.Fix:Use
raise ValueError(...)for anything a caller could get wrong. Reserveassertfor internal invariants.An exception disappears with no handler anywhere.
Why: A
return,breakorcontinuein afinallyblock discarded it.Fix:Move the control-flow statement out of the
finally. Python 3.14 warns about this.RuntimeError: No active exception to reraise.Why: A bare
raiseoutside anexceptblock.Fix:Give it something to raise, or move it inside the handler.
TypeError: exceptions must derive from BaseException.Why: An attempt to raise a class or object that is not an exception.
Fix:Inherit from
Exception. See guide 3.