Unravelling assertions
In this post, as part of my series on Python's syntactic sugar, I'm going to cover assert
statements. Now, the actual unravelling of the syntax for assert a, b
is already given to us by the language reference:
Since there isn't much to it, I'm going to spend this post mostly explaining what's going on with this unravelled code since there's a couple of details that you might not know.
To begin, __debug__
represents whether CPython was run with -O
or -OO
. These flags control the optimization level of CPython (hence the use of the letter "O" for them; saying it twice basically represents "more optimizations"). Without either flag specified, __debug__ == True
. But if you use either -O
or -OO
then __debug__ == False
(-OO
also strips out docstrings to save a tiny bit of memory).
The next thing to be aware of is that the error message argument is only executed if the assertion fails. This is why you can't just define an assert_()
function for the unravelling; like with and
and or
, you have to make sure not to execute b
unless not a
is true (and since raise
is a statement, we can't use our conditional operator trick like we did with and
and or
).
The unravelling is actually a bit simplistic because technically the lookup of __debug__
and AssertionError
is done directly from the builtins
module, not from the scope of the statement. So if you want to be really accurate in the unravelling, it's:
Finally, you might be wondering why I didn't write this as a single if
statement: if __debug__ and not a: raise AssertionError(b)
. Semantically they are equivalent, but in the bytecode that Python produces they are vastly different. Look at the bytecode for the unravelling proposed at the start:
4 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
Compare that to the bytecode for if not a and __debug__: raise AssertionError(b)
(and I chose that order for the and
to prove a point which you will see in a moment):
2 0 LOAD_GLOBAL 0 (a)
2 POP_JUMP_IF_TRUE 16
4 LOAD_CONST 1 (False)
6 POP_JUMP_IF_FALSE 16
3 8 LOAD_GLOBAL 1 (AssertionError)
10 LOAD_GLOBAL 2 (b)
12 CALL_FUNCTION 1
14 RAISE_VARARGS 1
>> 16 LOAD_CONST 0 (None)
18 RETURN_VALUE
One is a bit shorter than the other. 😉 Python's peephole optimizer notices if __debug__: ...
and will completely drop the statement if you run Python with -O
or higher (and just note that assert
statements are dropped by the peephole optimizer as well). Otherwise the peephole optimizer will replace __debug__
with the appropriate literal. That means there's a bit of wasted effort with if not a and __debug__: ...
since not a
will be evaluated before __debug__
. And even with the reverse order to and
you still have to deal with the test and jump for the if
conditional guard.
And that's it! And I would like to leave you with a piece of advice when it comes to assert
statements: never put actual logic that you couldn't stand to have not run in an assert
! Some of you might be 🙄, but I know of a multi-billion dollar company with a lot of Python which couldn't use -O
because it broke their code due to the removal of assert
statements (I don't know if they ever fixed that issue).