The warnings
module currently doesn’t offer a thread-safe (or async-safe) way of filtering warnings. Most seriously, the catch_warnings
context manager is not thread-safe.
Fixing this without breaking existing code is hard to do. There is much code that accesses, mutates and even assigns warnings.filters
. So, my initial strategy to fix this was to add an additional layer of filtering, using a contextvar
. See my draft PR for a prototype. The design is similar to what’s done for decimal
contexts. There is a local_context()
context manager that can be used like this:
with warnings.local_context() as ctx:
ctx.simplefilter(...)
...
In order to provide easier forwards compatibility (code that can work with both old and new versions of Python), the context class supports an API that overlaps with the warnings
module itself. That way, you can do things like this:
import warnings
try:
warn_ctx = warnings.get_context
except AttributeError:
def warn_ctx():
return warnings
with warn_ctx().catch_warnings():
warn_ctx().simplefilter(...)
...
with warn_ctx().catch_warnings(record=True) as log:
...
Some initial feedback on this approach suggests that the try/except block is ugly and there are many more uses of catch_warnings()
compared to code that accesses warnings.filters
. Searching the top 500 PyPI projects, there are only about six (6) lines of code that touch warnings.filters
while there are 700+ lines that use catch_warnings()
. So requiring all that code to switch to a new thread-safe context manager would seem quite disruptive compared with fixing the code that monkeys with warnings.filters
.
Based on this observation, I created an alternative PR that makes catch_warnings()
thread-safe. It does so by changing it to use a contextvar (thread-local, async safe). The amount code changes to make the CPython unit test suite pass was actually quite small. There are a number of potential issues with this approach though. Code like this would not work as expected, for example:
with warnings.catch_warnings():
warnings.simplefilter(...)
...
warnings.filters.pop(0)
...
In current CPython, threads don’t inherit the context from the spawning thread. So your warning filters would go back to the default when you spawn a thread. I suggest that we change that so threads inherit the context by default.