CertGrid CertGrid
Concepts·Certified Associate Python Programmer

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

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.

One machine, and any shell with Python 3 will do - these exams test the language, not a distribution.
Server NameIP AddressOSRolesCPURAMHDD
RUNNER01192.168.0.27Ubuntu 26.04 LTSPython 3.14.4 - the only machine this path needs2 Core4 GB50 GB

Before you start

  1. 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 result1, then caught: 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.

  2. A bare raise re-raises what you are handling

    Inside an except block, raise with 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 zero

    Expected resultlogging, then re-raising, then the outer handler catching the same exception.

    Success conditionYou can handle an exception partially and pass it on.

  3. A bare raise with nothing to re-raise

    Not a SyntaxError - a runtime RuntimeError, 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 resultRuntimeError: No active exception to reraise.

    Success conditionYou can name what a stray raise produces.

  4. raise from, to keep the original cause

    When you translate a low-level failure into a domain-specific one, raise ... from e records 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 resultcaught: could not parse the input, then cause: ValueError - invalid literal for int() with base 10: 'x'.

    Success conditionYou can chain one exception to another without losing information.

  5. assert

    assert condition, message raises AssertionError if 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 bare AssertionError with no message.

    Success conditionYou can write an assertion and read the error it produces.

  6. assert disappears under -O

    This is the fact that makes assert unsuitable for validating input. The -O flag 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 -O

    Expected resultexit 0 - the assertion was removed by -O - the assertion did not run at all.

    Success conditionYou know why assert must not be used to check user input.

  7. The assert that can never fail

    Because assert is 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 raised

    Expected resultA SyntaxWarning about the parentheses, then no AssertionError was raised - despite 1 == 2.

    Success conditionYou can spot an assertion that has been disabled by its own brackets.

  8. finally runs before the value is handed back

    A return inside try does not leave immediately - finally runs 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 try

    Expected resultfinally ran before the value was handed back, then from try.

    Success conditionYou can order the output of a function with return inside try.

  9. finally can discard an exception entirely

    If finally contains a return, it wins outright - and anything the try was 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 it

    Expected resultA SyntaxWarning, then finally swallowed it - and no traceback at all.

    Success conditionYou can explain how an exception can vanish without a handler.

  10. 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 return in both try and finally. The finally one.

    "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 raise do?" - re-raises the exception being handled.

    "What does a bare raise do outside a handler?" - RuntimeError.

    "When should you use assert rather than raise?" - for internal invariants, never for input validation.

    "Does finally run 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 ~/py

    Expected resultAn empty scratch directory.

    Success conditionYou can raise, assert and clean up deliberately.

Troubleshooting

Official sources