TL;DR Python’s assert
statement in a NOP in optimized mode (python -O
or python -OO
). There exists code that assumes this, and much much more code that is ignorant of this and broken when running with -O
. Lets do something about this, trying not to break existing code.
This was discussed here before at length without getting anywhere, so I’ll try to make a summary of the discussion and then offer some thoughts and a few ways forward.
-O
stripsassert
s and defines__debug__
asFalse
(andif False: stuff
is always stripped by the compiler),-OO
also strips docstrings- Guido is against changing the language, prefers to deprecate
-O
and-OO
- This gotcha is featured in lists of python security pitfalls
- The fact that it’s often not the same people who write the code and who run it (especially in the case of dependencies) makes it hard to push the idea that “it’s alright because you can always choose not to use
-O
”, and this alone steers people to use neither-O
nor assert. - There is at least one library (
click
) that relies on assertion stripping to run slow checks in performance-critical sections - There are multiple volunteers to write a PEP if a core dev would sponsor it
More things that were discussed
- Python’s
assert
was modeled after C’sassert()
macro, which is stripped by-DNDEBUG
(which is a separate argument than-O
)- Traditionally it was stripped from release builds [Editor’s Note: however, these days, at least commercial C/C++ projects tend to keep assertions in release, to the point that I consider it a best practice.
CPython
strips them) - The idea comes from Eiffel, where they serve a very specific purpose and have very specific semantics [EN: and some of them are kept in release by default and others stripped]
- Traditionally it was stripped from release builds [Editor’s Note: however, these days, at least commercial C/C++ projects tend to keep assertions in release, to the point that I consider it a best practice.
- Theoretically, if someone writes an assert that depends on user input and not just on the programmer not being wrong, they’re “using it wrong”
- In practice…
- Many have a dislike for code behaving differently in test and production, as it creates hard to debug issues
- Some people think
AssertionError
should have been aBaseException
and any code that handles it is suspicious, because if programmer assumptions have been broken, any bad thing might happen. In practice, web servers catch it and log an error and it’s probably wise
- The non-stripped assertion behavior can’t be easily replicated with a function, because
assert
only evaluates the second parameter if the assertion fails and functions always evaluate all arguments, also because an assert is very self-documenting in a traceback (when the bottom line of a traceback isassert author is not None
, there’s no need for a message “author is None”) - Rust has
assert!
anddebug_assert!
- Surprisingly,
pytest
assertions are not stripped with-O
because of their AST magic
I think @Tinche said it best:
As a person in charge of operating a large-scale Python deployment, am I expected to run with -O in production or not? If the answer is ‘yes’, I’ve been doing it wrong for a large number of years and we absolutely need to communicate this better, and it complicates the deployment story further. If the answer is ‘no’, asserts should always work.
My own novel thoughts:
assert
is my favorite python statement, and in 15 years of python the only place I’ve encountered -O
until today was in github issues asking to replace it with the cumbersome if not condition: raise AssertionError(msg)
Maybe if assertions are not “used wrong” we don’t need to run them in production, but I’ve yet to meet a CTO that would prefer the perf gains to the loud crashing behavior when the impossible does happen. This is Python, not C, if we cared about the performance tax of one more statement and a few variable lookups and attribute lookups, we’d be writing that function in Rust or something. Assertions that call a costly function are very rare.
However, assertions that are “using it wrong” are very common, which means -O
is completely broken and anyone using it does it at their own risk. stdlib
is full of assertions that check for things determined by the non-stdlib caller, which means they’re broken in -O
. See e.g. the Threading
module, GH-106236.
There are rare cases where we do care about them being optimized out in production, so may I propose that we add new syntax for that… how about:
assert not __debug__ or condition, message
(Joking, this is not new syntax, it’s an idiom that achieves exactly the same behavior and is more explicit about it)
So my proposal is:
- Deprecate
-O
and-OO
, replace with-O=1
and-O=2
that don’t stripassert
s - Recommend
assert not __debug__ or condition, message
as an alternative in the deprecation message and in the docs (so if you google__debug__
the first result will be an explanation of the idiom)
I volunteer to personally pore over the standard library and fix all uses where performance matters, though I couldn’t find any in a preliminary search. And also over click
and any other open source library who’s maintainer chimes in that they care about this.
So we have a deprecation that fixes many bugs in the stdlib
and in user code and requires (only) affected users to change how they call python
to indicate they’re aware of the change, an explicit way to opt-in to the surprising behavior that already works, and even if someone misses the deprecation, in theory the only change they’ll experience is a performance regression, and in practice they might also get their bacon saved by an explicit crash instead of silent misbehavior in production.
Will someone sponsor this?