Skip to content
Home Python Python assert — Production Data Silently Corrupted by -O

Python assert — Production Data Silently Corrupted by -O

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Exception Handling → Topic 4 of 5
Negative ages in user database caused by Python's -O removal of assert statements, causing downstream analytics to produce impossible results — GFG won't.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
Negative ages in user database caused by Python's -O removal of assert statements, causing downstream analytics to produce impossible results — GFG won't.
  • Use raise for any condition that can legitimately occur at runtime — bad input, missing files, network failures. It's always active, even in optimized production builds.
  • assert is a developer communication tool, not a runtime guard. It documents invariants that should be impossible if your code is correct, and it disappears entirely with python -O.
  • Always pick the most specific built-in exception type (ValueError, TypeError, FileNotFoundError) or build a custom exception hierarchy — generic Exception forces callers to catch too broadly.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • raise triggers exceptions explicitly for runtime errors
  • assert documents developer invariants and is disabled in optimized builds (-O)
  • Use raise for any condition that can legitimately happen in production
  • Use assert only for conditions that should never be false if code is correct
  • Performance impact: assert overhead is negligible, but -O flag makes it vanish entirely
  • Biggest mistake: using assert for user input validation — it disappears, causing silent data corruption
🚨 START HERE

Quick Debug Cheat Sheet for raise/assert

Immediate commands and fixes for common exception and assertion issues.
🟡

Assertion not firing in production

Immediate ActionCheck if Python is running with -O by inspecting sys.flags.optimize
Commands
print(sys.flags.optimize)
python -c 'print(__debug__)'
Fix NowReplace assert with if not condition: raise AssertionError('msg') to ensure it runs regardless of flags.
🟡

Lost traceback when wrapping exception

Immediate ActionLook for raise MyCustomError('...') without from original_error in the except block.
Commands
Check the except block: is there a 'raise SomeError()' without 'from'?
Use traceback.print_exc() to see if chain is intact.
Fix NowChange to raise MyCustomError('message') from original_error.
🟡

Catching too broadly (except Exception) masks bugs

Immediate ActionScan except clauses for bare except Exception: which catches everything including KeyboardInterrupt.
Commands
grep -rn 'except Exception' src/
Check if handlers log the exception or re-raise appropriately.
Fix NowReplace with specific exception types or use except Exception as e: logging.exception('...') and re-raise if not handled.
Production Incident

The Silent Age: How Assert Disappeared in Production with -O

A team used assert to validate user age. When deployed with python -O, the assertions vanished, allowing negative ages to enter the system and corrupt analytics.
SymptomNegative ages appeared in the user database, causing downstream analytics to produce impossible results (e.g., average age of -3). No exceptions were raised during registration.
AssumptionThe team assumed assert works at runtime regardless of the Python invocation. They had never tested with the -O flag.
Root causePython's -O (optimize) flag removes all assert statements at compile time. The validation was completely absent in production.
FixReplace all assert-based input validation with explicit if-raise statements: if age < 0: raise ValueError('age must be positive'). Added integration tests that run both with and without -O.
Key Lesson
Never use assert to validate external input. Assert is for developer contracts, not user data validation.Always include a test suite that runs with python -O to catch disappearing assertions.Treat assert as a development-only debugging tool, not a runtime guard.
Production Debug Guide

Symptom → Action guide for common exception handling problems

An exception is raised but not caught by any except handler — program crashes with unhandled exception.Check that the exception type in the except clause matches exactly or is a parent of the raised type. Use a generic except Exception as last resort in top-level handlers.
An assert statement runs in development but appears to have no effect in production.Check if Python is invoked with -O. If so, assertions are disabled. Replace assert with if-raise for production-critical checks.
A custom exception is caught, but the original traceback is lost — you see only 'During handling of the above exception, another exception occurred' without the root cause.Use raise NewException('message') from original_error to chain exceptions and preserve the full chain.
Catch block catches an exception but re-raises the wrong type, confusing callers.Do not use a bare raise inside except if you want to change the exception type. Use raise TypeError from original_error to wrap.

Every program you write makes assumptions — that a user's age is positive, that a file actually exists, that a payment amount isn't zero. When those assumptions break, your program shouldn't silently limp along producing garbage output. It should fail loudly, clearly, and in a way that points directly at the problem. That's exactly what raise and assert are built for, and knowing the difference between them is what separates defensive code from fragile code.

The real problem isn't that Python will crash when things go wrong — it's that without raise and assert, it often crashes in the wrong place, showing you a confusing error three function calls away from where the actual bug lives. These two tools let you put guardrails exactly where your assumptions live, so when something breaks, the error message is a signpost, not a riddle.

By the end of this article you'll know how to raise built-in and custom exceptions with meaningful messages, how to use assert as a development-time safety net, why you should never use assert to validate user input, and how to chain exceptions to preserve debugging context. You'll be writing code that fails helpfully instead of mysteriously.

raise — Telling Python Something Has Gone Wrong

raise is how you deliberately trigger an exception. You're not waiting for Python to stumble — you're the one blowing the whistle because you've already detected the problem.

The basic form is raise ExceptionType("your message"). The exception type tells Python (and the developer reading the traceback) what kind of problem occurred. The message tells them what the specific problem was. Both matter — a ValueError with the message "age must be a positive integer, got -3" is infinitely more useful than a generic crash.

Python has a rich built-in exception hierarchy. Choosing the right type isn't just cosmetic — it lets callers catch specific exceptions without catching everything. Use ValueError when a value is the wrong kind. Use TypeError when the wrong type was passed. Use RuntimeError when something goes wrong that doesn't fit a neater category. Using the right exception type is a form of documentation.

You can also raise inside an except block to re-raise after logging, or to wrap a low-level exception in a higher-level one that makes more sense to the caller.

user_age_validator.py · PYTHON
1
def register_user(username: str
▶ Output
Success: {'username': 'alice'
💡Pro Tip:
Always include the actual bad value in your exception message. "age must be positive" forces the developer to add a print statement to find the value. "age must be positive, got -5" puts the value right in the traceback where it's instantly visible.
📊 Production Insight
Using a generic Exception type when raising forces callers to catch everything with except Exception, which swallows unrelated errors like KeyboardInterrupt or MemoryError.
Always pick the most specific built-in exception that describes the problem.
Rule: If you're tempted to use raise Exception(...), create a custom exception instead.
🎯 Key Takeaway
Use the most specific built-in exception that matches the problem.
Raise with a message that includes the actual invalid value.
Never use a bare raise outside an except block — it'll crash with RuntimeError.
Choosing the right exception type for raise
IfThe value has the wrong type (e.g., string passed for an int)
UseRaise TypeError
IfThe value has the right type but wrong range or format
UseRaise ValueError
IfThe operation is not allowed semantically
UseRaise RuntimeError or a custom exception
IfThe file or resource is missing
UseRaise FileNotFoundError or IOError

Custom Exceptions — Making raise Even More Powerful

Built-in exceptions are great, but they're generic. When you're building a library, an API client, or any system with its own domain logic, callers need to distinguish your errors from Python's built-in ones.

Creating a custom exception is just one line: subclass Exception (or a more specific built-in). That's it. You've now given your package its own exception namespace. Callers can except PaymentError and know they're handling your domain problem, not some random ValueError from an unrelated library.

The real power is in the hierarchy. A payment system might have a base PaymentError, with InsufficientFundsError and CardDeclinedError as subclasses. Callers who care about both can catch PaymentError. Callers who need to handle each case differently can catch the subclass. This is the same design Python itself uses — you can catch OSError for any file system problem, or FileNotFoundError specifically.

Add raise ... from original_error (exception chaining) when you're wrapping a low-level exception. This preserves the original traceback so debugging context is never lost — a sign of professional-grade code.

statistics_calculator.py · PYTHON
1
def calculate_weighted_average(values: list
▶ Output
Weighted grade: 81.50
AssertionError caught (dev bug, not user error): values and weights must be the same length: got 3 values and 2 weights
⚠ Watch Out:
Never write assert user_age > 0, "age must be positive" to validate user input. Run Python with python -O your_script.py and that assertion silently vanishes — your validation is gone and bad data flows straight through. Use raise ValueError for anything coming from outside your own code.
📊 Production Insight
Assertions silently vanish with python -O. If you rely on an assert for correctness, production may behave differently than development.
Always test your application with python -O in CI to catch disappearing assertions.
Rule: If removing the assert would cause data corruption, it should be a raise, not an assert.
🎯 Key Takeaway
Use assert for developer contracts that should never fire if code is correct.
Never use assert for input validation or any data from outside your code.
Test your application with python -O to ensure assert removal doesn't break logic.
When to use assert vs raise for precondition checks
IfCheck is against user input, file data, or external API response
UseUse raise with descriptive exception
IfCheck is a programmer contract that should never fail if code is correct
UseUse assert (development-time guard)
IfCheck must survive python -O flag in production
UseUse raise — assert will be removed
IfCheck is a post-condition after an internal algorithm
UseUse assert — it documents the invariant

raise vs assert — Choosing the Right Tool for the Job

The confusion between raise and assert is one of the most common intermediate-level mistakes in Python. They look similar — both stop execution when something is wrong — but they exist for completely different reasons and different audiences.

raise is for runtime conditions that can legitimately happen in production and that your code needs to handle gracefully. Bad user input, missing files, network failures, invalid API responses — these aren't bugs, they're expected failure modes. raise communicates the problem to the caller so they can decide what to do next.

assert is for invariants that should never be false if your code is correct. It's a communication tool between developers, not a runtime guard. It says: "I wrote this function assuming X is always true here. If X is ever false, the logic of this program is broken and we need to fix the code, not handle the error."

A useful mental model: raise handles the unexpected-but-possible. assert documents the impossible. If you find yourself writing assert to handle something that a user, file, or network could cause — swap it for raise. If you find yourself catching AssertionError in production code — that's a red flag that you've misused assert.

inventory_system.py · PYTHON
12345678910111213141516171819202122
import json

# ============================================================
# raise: for conditions that can happen legitimately at runtime
# ============================================================

def load_product_catalog(filepath: str) -> dict:
    try:
        with open(filepath, "r") as file:
            catalog = json.load(file)
    except FileNotFoundError:
        raise FileNotFoundError(
            f"Product catalog not found at '{filepath}'. Check the path and ensure the file was deployed."
        )
    except json.JSONDecodeError as parse_error:
        raise ValueError(
            f"Catalog file at '{filepath}' contains invalid JSON."
        ) from parse_error
    return catalog


def apply_discount(product: dict

Exception Chaining — raise X from Y Preserves the Full Story

When you catch a low-level exception and raise a domain-specific one, you have a choice: lose the original traceback or preserve it. A bare raise MyError('msg') discards the original exception entirely. raise MyError('msg') from original_error chains them — the traceback shows both exceptions, with 'The above exception was the direct cause of the following exception' linking them.

This is crucial for debugging. Without chaining, you see PaymentError and have to guess the root cause. With chaining, you see not only that the payment failed (PaymentError) but also why (KeyError because account was missing from the database). Exception chaining is a hallmark of production-grade error handling.

Python supports implicit chaining as well: when a new exception is raised while an exception is already active, Python automatically sets the __context__ attribute. But explicit chaining with from is preferred because it makes the causal relationship intentional and clear to readers.

config_loader.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
import json

# --- Custom exceptions ---
class ConfigError(Exception):
    """Base for all configuration errors."""
    pass

class ConfigParseError(ConfigError):
    """Raised when a config file cannot be parsed."""
    pass

class ConfigMissingError(ConfigError):
    """Raised when a required config key is missing."""
    pass


# --- Function that demonstrates proper chaining ---

def load_app_config(filepath: str) -> dict:
    try:
        with open(filepath) as f:
            config = json.load(f)
    except FileNotFoundError as fnf:
        raise ConfigError(f"Configuration file '{filepath}' not found") from fnf
    except json.JSONDecodeError as je:
        raise ConfigParseError(f"Invalid JSON in '{filepath}'") from je

    # Validate required keys
    if 'database_url' not in config:
        raise ConfigMissingError(
            "Config must contain 'database_url'"
        )

    return config


# --- Without chaining (bad) vs with chaining (good) ---

print("=== With chaining (raise ... from) ===")
try:
    load_app_config("nonexistent.json")
except ConfigError as e:
    print(repr(e))
    print(f"Cause: {e.__cause__}")

print("\n=== Without chaining (bare raise) — compare ===")
try:
    # Simulate bad pattern
    try:
        with open("nonexistent.json") as f:
            pass
    except FileNotFoundError as fnf:
        raise ConfigError("Config file not found")  # No 'from' — bad!
        # Raises ConfigError without linking the original error
except ConfigError as e:
    print(repr(e))
    print(f"Cause (missing): {e.__cause__}")
▶ Output
=== With chaining (raise ... from) ===
ConfigError('Configuration file \'nonexistent.json\' not found')
Cause: FileNotFoundError(2, 'No such file or directory')

=== Without chaining (bare raise) — compare ===
ConfigError('Config file not found')
Cause (missing): None
💡Pro Tip:
When re-raising in a finally block, use raise alone to re-raise the currently active exception. Introducing a new raise in a finally block can shadow the original exception — unless you use raise ... from to link them.
📊 Production Insight
A service masked a database connection error by wrapping it in a generic ServiceError without chaining. Developers spent hours assuming the service logic was wrong, when the real issue was a network partition.
Adding raise ... from cut debugging time by 80%.
Rule: Every time you wrap an exception, use raise ... from unless you have a specific reason not to.
🎯 Key Takeaway
Always chain exceptions when wrapping: raise DomainException from low_level_error.
Exception chaining preserves the full cause chain for debugging.
A bare raise inside except re-raises the current active exception unchanged.
When to use raise ... from
IfYou are catching one exception and raising a different one
UseUse raise NewError from original_error to preserve the cause
IfYou want to add context without changing the exception type
UseRe-raise with bare raise after logging, don't create a new exception
IfYou are re-raising in an except block without changing the type
UseUse bare raise to preserve original traceback
IfYou are raising in a finally block while an exception is active
UseUse raise ... from to chain, else the new exception can shadow the original
🗂 raise vs assert
The definitive comparison for production use
Feature / Aspectraiseassert
Primary purposeSignal a runtime error to the callerDocument a programmer assumption / invariant
Target audienceCallers of your function (including production code)Fellow developers reading or testing the code
Disabled in production?Never — always activeYes — silently removed with python -O flag
Exception type raisedAny exception you choose (ValueError, TypeError, custom, etc.)Always AssertionError — you can't choose another type
Right use caseInvalid user input, missing files, API failuresPost-conditions, algorithm invariants, pre-conditions you control
Catchable in production?Yes — callers can and should catch specific exceptionsCatching AssertionError in production is a code smell
Carries structured data?Yes — custom exceptions can hold attributesOnly a string message
Exception chaining?Yes — raise X from Y preserves original tracebackNot applicable
In test code?Use raise in the code being testedUse assert in test assertions (pytest style)

🎯 Key Takeaways

  • Use raise for any condition that can legitimately occur at runtime — bad input, missing files, network failures. It's always active, even in optimized production builds.
  • assert is a developer communication tool, not a runtime guard. It documents invariants that should be impossible if your code is correct, and it disappears entirely with python -O.
  • Always pick the most specific built-in exception type (ValueError, TypeError, FileNotFoundError) or build a custom exception hierarchy — generic Exception forces callers to catch too broadly.
  • Use raise NewError('...') from original_error whenever you wrap a low-level exception — exception chaining preserves the full traceback chain so debugging never starts from scratch.

⚠ Common Mistakes to Avoid

    Using assert to validate user input
    Symptom

    assert user_age > 0 looks like a guard but vanishes entirely when Python runs with the -O flag, silently allowing negative ages through.

    Fix

    Replace all input validation with if not condition: raise ValueError("..."). Reserve assert only for internal programmer contracts.

    Raising the wrong exception type
    Symptom

    Raising Exception('username is empty') instead of ValueError('username is empty') forces callers to catch the overly broad Exception class, which swallows every possible error including bugs.

    Fix

    Always pick the most specific built-in exception that fits (ValueError for bad values, TypeError for wrong types, FileNotFoundError for missing files), or create a custom exception subclass.

    Losing the original exception when wrapping
    Symptom

    Writing except SomeError: raise MyCustomError('something went wrong') discards the original traceback entirely, making the root cause invisible during debugging.

    Fix

    Always use raise MyCustomError('...') from original_error to chain exceptions and preserve the full diagnostic trail.

Interview Questions on This Topic

  • QWhat is the practical difference between raise and assert in Python, and why should you never use assert to validate user input?SeniorReveal
    raise is a runtime statement that always executes and raises a specified exception. assert is a debugging aid that raises AssertionError only if the condition is false, but it is entirely removed when Python runs with -O. Therefore, assert should never be used to validate data from outside your code because the validation disappears in production. Use raise with an appropriate exception type (ValueError, TypeError, etc.) for all input validation.
  • QHow does exception chaining work with raise ... from ..., and why is it important in real-world applications?SeniorReveal
    Exception chaining with raise X from Y preserves the original exception as the cause of the new one. This is important because when you catch a low-level error (like a database connection failure) and raise a domain-specific error (like PaymentError), without chaining the original traceback is lost, making debugging much harder. The from keyword creates an implicit __cause__ attribute, and Python's traceback display shows the full chain: 'The above exception was the direct cause of the following exception'.
  • QIf assert statements are disabled with the -O flag, what does it mean if you find production code that depends on an assert not being skipped — and how would you fix it?SeniorReveal
    It means the production code has a critical bug: it depends on a check that can be silently disabled. The fix is to replace all such assert statements with explicit if not condition: raise SomeError(...) (often ValueError or a domain exception). Additionally, the team should add a CI step that runs the test suite with python -O to catch any remaining dependency on assertions. Finally, the codebase should adopt a policy: assert is only for internal developer contracts, never for runtime validation.

Frequently Asked Questions

Can I catch an AssertionError in Python?

Technically yes — except AssertionError is valid syntax. But catching AssertionError in production code is almost always a design mistake. If an assertion fires, it means your code has a logic bug that needs to be fixed, not silently swallowed. If you need catchable errors, use raise instead of assert.

What happens if I use raise without an argument inside an except block?

A bare raise inside an except block re-raises the current active exception unchanged, including its original traceback. It's useful when you want to log an error but still let it propagate to the caller. Outside an except block, a bare raise causes a RuntimeError: No active exception to re-raise.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousCustom Exceptions in PythonNext →Context Managers in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged