Assertions and Exceptions are not the same
What’s wrong with the
withdraw_money function in the code below?
If you run it with
python bank.py you’re given an error message:
Which, at first sight, is correct: I am withdrawing
20.0 from a client account with a balance of
10.0, and the code is specifically written to exit if the withdrawal would put the client account into the red. So what’s the problem, then?
What’s an assertion?
Well, assertions are programmer aids that test for system invariants that must never occur in production. That means they are designed to catch and terminate the program if any of the programmer-specified assertions occur. They are often – in computer games and other large applications where performance is key – used in hot parts of the code where they often add a significant performance burden during development in return for exiting the application with additional debug information – something that is often much harder to get at easily in compiled languages – if an assertion error were to occur.
That’s why they’re stripped out of the code by the compiler during a release (or production) build. Now I’m sure you can see where I’m going with this…
A little-known feature of
CPython (the official reference version of Python we all know and love) is the
-O optimization flag you can pass to the
python program on startup. It’s supposed to “optimize” the bytecode generated by Python but it adds little benefit, so most don’t use it, or even know about its existence.
So what happens if we rerun the script from before but with optimizations enabled:
Oops. The assertion’s elided from the bytecode and our code carried on without a care in the world.
Disassembling the optimized function
Disassembling the optimized
withdraw_money function makes it obvious the assert statement is gone:
The column on the left indicate line numbers and correspond to the highlighted lines:
Try comparing the disassembled output from
dis.dis with the unoptimized version.
If you currently do this in your code, the solution is simple: raise an exception explicitly and capture it further up the call tree where you can act on the error directly. Don’t take that to mean that you should never use assertions — on the contrary, they are very useful. But they are programmer aids and not designed for general error handling.
Saying that, it’s easy to see why people get confused because
assert itself raises an exception called
AssertionError which does little to resolve the confusion between the two modes of control flow.
- Assertions are programmer aids, not error handlers for your business logic
Asserts help programmers (and testers) spot invariants in your code that should never, ever happen once the code is stable and deployed. Use it to catch programming mistakes – ranging from the likely to improbable – liberally.
- Assertions can be toggled on or off at will
So never use an
assertwhen another method of capturing and raising an error is possible. Always write your code with the assumption that the assertions could disappear at any minute. And if you’re unsure, you can run your program with
python -Oor by setting
PYTHONOPTIMIZE=1to test if it still works.
- Avoid side effects in assert statements
You should never cause side effects – meaning, something that modifies the state of something else, like creating a user in a database – in an assertion. It will result in serious, unintuitive errors in your code as the side-effected code ceases to exist if your code is run with assertions disabled.