Consider a very simple function:
def generate_something(data):
    if data is None:
        raise Exception('No data!')
    return MyObject(data)
Its output is basically an instance of an object I want to create or an exception if the function cannot create the object. We can say that the output is binary since it either succeeds (and gives back an object) or not (and gives back an Exception).
What is the most pythonic way to handle a third state, that is "success but with some warnings"?
def generate_something(data):
    warnings = []
    if data is None:
        raise Exception("No data!")
    if data.value_1 == 2:
        warnings.append('Hmm, value_1 is 2')    
    if data.value_2 == 1:
        warnings.append('Hmm, value_2 is 1')    
    return MyObject(data), warnings
Is returning a tuple the only way to handle this, or it is possible to broadcast or yield warnings from within the functions and catch them from the caller?
warnings
Python has a built-in warning mechanism implemented in the warnings module. The problem with this is that warnings maintains a global warnings filter, which might unintenionally cause the warnings your function throws to be suppressed. Here's a demonstration of the problem:
import warnings
def my_func():
    warnings.warn('warning!')
my_func()  # prints "warning!"
warnings.simplefilter("ignore")
my_func()  # prints nothing
If you want to use warnings regardless of this, you can use warnings.catch_warnings(record=True) to collect all thrown warnings in a list:
with warnings.catch_warnings(record=True) as warning_list:
    warnings.warn('warning 3')
print(warning_list)  # output: [<warnings.WarningMessage object at 0x7fd5f2f484e0>]
For the reason explained above, I recommend rolling your own warning mechanism instead. There are various ways to implement this:
Just return a list of warnings
The easiest solution with the least overhead: Just return the warnings.
def example_func():
    warnings = []
    if ...:
        warnings.append('warning!')
    return result, warnings
result, warnings = example_func()
for warning in warnings:
    ...  # handle warnings
Pass a warning handler to the function
If you want to handle the warnings immediately when they're generated, you can rewrite your function to accept a warning handler as argument:
def example_func(warning_handler=lambda w: None):
    if ...:
        warning_handler('warning!')
    return result
def my_handler(w):
    print('warning', repr(w), 'was produced')
result = example_func(my_handler)
contextvars (python 3.7+)
With python 3.7 we got the contextvars module, which lets us implement a higher-level warning mechanism based on context managers:
import contextlib
import contextvars
import warnings
def default_handler(warning):
    warnings.warn(warning, stacklevel=3)
_warning_handler = contextvars.ContextVar('warning_handler', default=default_handler)
def warn(msg):
    _warning_handler.get()(msg)
@contextlib.contextmanager
def warning_handler(handler):
    token = _warning_handler.set(handler)
    yield
    _warning_handler.reset(token)
Usage example:
def my_warning_handler(w):
    print('warning', repr(w), 'was produced')
with warning_handler(my_warning_handler):
    warn('some problem idk')  # prints "warning 'some problem idk' was produced"
warn(Warning('another problem'))  # prints "Warning: another problem"
Caveats: As of now, contextvars doesn't support generators. (Relevant PEP.) Things like the following example won't work correctly:
def gen(x):
    with warning_handler(x):
        for _ in range(2):
            warn('warning!')
            yield
g1 = gen(lambda w: print('handler 1'))
g2 = gen(lambda w: print('handler 2'))
next(g1)  # prints "handler 1"
next(g2)  # prints "handler 2"
next(g1)  # prints "handler 2"
without contextvars (for python <3.7)
If you don't have contextvars, you can use this async-unsafe implementation instead:
import contextlib
import threading
import warnings
def default_handler(warning):
    warnings.warn(warning, stacklevel=3)
_local_storage = threading.local()
_local_storage.warning_handler = default_handler
def _get_handler():
    try:
        return _local_storage.warning_handler
    except AttributeError:
        return default_handler
def warn(msg):
    handler = _get_handler()
    handler(msg)
@contextlib.contextmanager
def warning_handler(handler):
    previous_handler = _get_handler()
    _local_storage.warning_handler = handler
    yield
    _local_storage.warning_handler = previous_handler
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With