3.3 Raising Exceptions and Assertions
Key Takeaways
- The raise statement explicitly triggers exceptions using an exception class, an instantiated exception object, or a bare raise inside an active handler.
- A bare raise statement re-raises the currently active exception preserving its original traceback; calling bare raise outside an active except handler causes a RuntimeError.
- The assert statement tests invariant boolean conditions during execution, raising an AssertionError if the evaluated expression is falsy.
- Assertions must never be used for user input validation or security authorization because the Python interpreter removes all assert statements when run with the -O (optimize) flag.
- Exception instances store all constructor arguments in their .args attribute tuple, which can be inspected programmatically during error handling.
Raising Exceptions and Assertions
While Python automatically raises exceptions when runtime errors occur, developers frequently need to generate errors explicitly to signal invalid states, enforce domain constraints, or re-raise caught exceptions after logging. Python provides two primary statements for programmatic error signaling: raise and assert.
The raise Statement Mechanics
The raise statement forces an exception to occur immediately. It supports three distinct syntactic forms.
1. Raising by Class Name
When raise is followed by an exception class name without arguments, Python automatically instantiates the class with zero constructor arguments:
raise ValueError
# Exactly equivalent to:
raise ValueError()
2. Raising an Instantiated Object
You can explicitly instantiate the exception class, passing diagnostic messages or error payloads to its constructor:
raise ValueError("User age must be a positive integer")
3. Re-raising with Bare raise
A bare raise keyword (with no expression following it) re-raises the currently active exception in the local scope. This pattern is widely used to inspect, log, or perform partial cleanup before allowing the error to propagate upward:
def process_transaction(account_id, amount):
try:
debit_account(account_id, amount)
except (ConnectionError, DatabaseError) as err:
logger.error(f"Transaction failed for {account_id}: {err}")
# Re-raise the exact same exception instance with original traceback intact
raise
[!CAUTION] The Bare
raiseTrap: A bareraiseis only legal inside an activeexcepthandler. If you execute a bareraisestatement outside of any exception handling block (or after the exception handler has finished and the exception was cleared), Python raises aRuntimeError: No active exception to reraise.
# ILLEGAL: Bare raise outside except block
def invalid_reraise():
raise # Raises RuntimeError: No active exception to reraise
Exception Instances and the .args Attribute
When an exception is instantiated, all arguments passed to the constructor are stored as a tuple in the .args attribute of the exception instance.
try:
# Pass multiple distinct values to the constructor
raise ValueError("ERR_INVALID_PORT", 8080, "Port number out of range")
except ValueError as exc:
print(f"Type of args: {type(exc.args)}") # <class 'tuple'>
print(f"Content of args: {exc.args}") # ('ERR_INVALID_PORT', 8080, 'Port number out of range')
print(f"First argument: {exc.args[0]}") # ERR_INVALID_PORT
print(f"String representation: {exc}") # ('ERR_INVALID_PORT', 8080, 'Port number out of range')
When an exception is instantiated with a single argument (like raise ValueError("Invalid id")), exc.args is a 1-tuple ("Invalid id",), and str(exc) returns just the string "Invalid id".
The assert Statement
The assert statement is an internal debugging aid. It tests an expression; if the expression evaluates to False (or a falsy value), Python raises an AssertionError.
Formal Syntax
assert condition[, optional_error_message]
Under the hood, the Python interpreter evaluates an assert statement using the following internal translation logic:
if __debug__:
if not condition:
raise AssertionError(optional_error_message)
Examples of Assertions
def calculate_discount(price, discount_rate):
# Internal invariant: discount must be between 0.0 and 1.0
assert 0.0 <= discount_rate <= 1.0, f"Invalid rate: {discount_rate}"
assert price >= 0, "Price cannot be negative"
return price * (1.0 - discount_rate)
print(calculate_discount(100, 0.2)) # Output: 80.0
# calculate_discount(100, 1.5) # Raises AssertionError: Invalid rate: 1.5
The Dangerous Tuple Assertion Trap
A notorious Python pitfall—frequently tested on the PCAP exam—is passing a parenthesized tuple to assert:
# DANGEROUS BUG:
assert (x > 0, "x must be positive")
In Python, a non-empty tuple (condition, "message") always evaluates to truthy boolean True, regardless of whether x > 0 is True or False! As a result, this assertion will never fail, completely defeating the check.
# Demonstration of the Tuple Trap:
x = -999
assert (x > 0, "x must be positive") # DOES NOT RAISE! The tuple evaluates to True!
# Correct syntax (no enclosing parentheses around condition and message):
# assert x > 0, "x must be positive" # Properly raises AssertionError
Appropriate vs Inappropriate Uses of assert
Understanding when to use assert versus raising standard exceptions (ValueError, TypeError) is critical for software engineering and PCAP examination scenarios.
| Attribute | assert Statement | Standard raise Statement |
|---|---|---|
| Primary Purpose | Internal invariant and sanity checks during development | Operational error handling and input validation |
| Target Audience | Developers and test suites | Callers, clients, and end-users |
| Optimization Behavior | Stripped completely when running with -O flag | Always executed regardless of interpreter flags |
| Exception Raised | AssertionError | Specific types (ValueError, KeyError, etc.) |
The Impact of the -O (Optimization) Flag
When Python is launched with the -O (optimize) or -OO flag, the built-in constant __debug__ is set to False, and the interpreter completely strips out and ignores all assert statements during bytecode generation (.pyc compilation).
# Standard run: assertions are active
python script.py
# Optimized run: ALL assert statements are stripped from bytecode!
python -O script.py
[!WARNING] Security and Validation Anti-Pattern: Never use
assertto validate user input, verify passwords, or enforce security authorization. If a server or deployment runs withpython -O, all security checks built withassertwill be skipped, creating critical vulnerabilities!
What happens when a bare raise statement is executed outside of any except block?
What occurs when the following code is executed? x = -10 assert (x > 0, 'x must be positive') print('Passed')
What is the effect of running a Python script with the -O (optimize) command-line flag?
Given the following code snippet: try: raise KeyError('missing_id', 404) except LookupError as exc: print(exc.args) What is the exact output?