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.
Last updated: August 2026

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 raise Trap: A bare raise is only legal inside an active except handler. If you execute a bare raise statement outside of any exception handling block (or after the exception handler has finished and the exception was cleared), Python raises a RuntimeError: 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.

Attributeassert StatementStandard raise Statement
Primary PurposeInternal invariant and sanity checks during developmentOperational error handling and input validation
Target AudienceDevelopers and test suitesCallers, clients, and end-users
Optimization BehaviorStripped completely when running with -O flagAlways executed regardless of interpreter flags
Exception RaisedAssertionErrorSpecific 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 assert to validate user input, verify passwords, or enforce security authorization. If a server or deployment runs with python -O, all security checks built with assert will be skipped, creating critical vulnerabilities!

Loading diagram...
Decision Tree: raise vs assert
Test Your Knowledge

What happens when a bare raise statement is executed outside of any except block?

A
B
C
D
Test Your Knowledge

What occurs when the following code is executed? x = -10 assert (x > 0, 'x must be positive') print('Passed')

A
B
C
D
Test Your Knowledge

What is the effect of running a Python script with the -O (optimize) command-line flag?

A
B
C
D
Test Your Knowledge

Given the following code snippet: try: raise KeyError('missing_id', 404) except LookupError as exc: print(exc.args) What is the exact output?

A
B
C
D