Python assert — Production Data Silently Corrupted by -O
- Use
raisefor any condition that can legitimately occur at runtime — bad input, missing files, network failures. It's always active, even in optimized production builds. assertis a developer communication tool, not a runtime guard. It documents invariants that should be impossible if your code is correct, and it disappears entirely withpython -O.- Always pick the most specific built-in exception type (
ValueError,TypeError,FileNotFoundError) or build a custom exception hierarchy — genericExceptionforces callers to catch too broadly.
- 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
Quick Debug Cheat Sheet for raise/assert
Assertion not firing in production
print(sys.flags.optimize)python -c 'print(__debug__)'Lost traceback when wrapping exception
Check the except block: is there a 'raise SomeError()' without 'from'?Use traceback.print_exc() to see if chain is intact.Catching too broadly (except Exception) masks bugs
grep -rn 'except Exception' src/Check if handlers log the exception or re-raise appropriately.Production Incident
if age < 0: raise ValueError('age must be positive'). Added integration tests that run both with and without -O.Production Debug GuideSymptom → Action guide for common exception handling problems
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.
def register_user(username: str
"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.Exception type when raising forces callers to catch everything with except Exception, which swallows unrelated errors like KeyboardInterrupt or MemoryError.raise Exception(...), create a custom exception instead.raise outside an except block — it'll crash with RuntimeError.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.
def calculate_weighted_average(values: list
AssertionError caught (dev bug, not user error): values and weights must be the same length: got 3 values and 2 weights
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.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.
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.
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__}")
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
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.ServiceError without chaining. Developers spent hours assuming the service logic was wrong, when the real issue was a network partition.raise ... from cut debugging time by 80%.raise ... from unless you have a specific reason not to.raise DomainException from low_level_error.| Feature / Aspect | raise | assert |
|---|---|---|
| Primary purpose | Signal a runtime error to the caller | Document a programmer assumption / invariant |
| Target audience | Callers of your function (including production code) | Fellow developers reading or testing the code |
| Disabled in production? | Never — always active | Yes — silently removed with python -O flag |
| Exception type raised | Any exception you choose (ValueError, TypeError, custom, etc.) | Always AssertionError — you can't choose another type |
| Right use case | Invalid user input, missing files, API failures | Post-conditions, algorithm invariants, pre-conditions you control |
| Catchable in production? | Yes — callers can and should catch specific exceptions | Catching AssertionError in production is a code smell |
| Carries structured data? | Yes — custom exceptions can hold attributes | Only a string message |
| Exception chaining? | Yes — raise X from Y preserves original traceback | Not applicable |
| In test code? | Use raise in the code being tested | Use assert in test assertions (pytest style) |
🎯 Key Takeaways
- Use
raisefor any condition that can legitimately occur at runtime — bad input, missing files, network failures. It's always active, even in optimized production builds. assertis a developer communication tool, not a runtime guard. It documents invariants that should be impossible if your code is correct, and it disappears entirely withpython -O.- Always pick the most specific built-in exception type (
ValueError,TypeError,FileNotFoundError) or build a custom exception hierarchy — genericExceptionforces callers to catch too broadly. - Use
raise NewError('...') from original_errorwhenever you wrap a low-level exception — exception chaining preserves the full traceback chain so debugging never starts from scratch.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the practical difference between
raiseandassertin Python, and why should you never useassertto validate user input?SeniorReveal - QHow does exception chaining work with
raise ... from ..., and why is it important in real-world applications?SeniorReveal - QIf
assertstatements are disabled with the-Oflag, what does it mean if you find production code that depends on anassertnot being skipped — and how would you fix it?SeniorReveal
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.
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.