Add `with_notes` method to exception classes

Currently, it is non-trivial to attach a large number of notes to an exception from an iterable. Calling add_note for each item looks ugly and many method calls incur overhead. Fortunately, the notes are stored in the __notes__ attribute of the exception, which is a list of strings. But that attribute doesn’t exist when there were no notes; it is only added on the first add_note call! Therefore, I propose to add a with_notes method to BaseException that takes an iterable and returns the original exception after the addition of the notes, just like with_traceback sets the traceback and returns the same exception object. Something like:

def with_notes(self, notes, _=object()):
    if (first := next(notes := iter(notes), _)) is _: # check against a sentinel
        return # no notes to add
    self.add_note(first) # create the __notes__ attribute if it doesn't exist
    self.__notes__.extend(notes) # works because __notes__ is always a list

Isn’t this a bit over engineered? How many notes are you adding for this to become a performance issue? Are your exceptions not exceptional enough? Is this something you’ve profiled?

for note in notes:
    exc.add_note(note)

Alternatively, maybe combine all notes into a single large note. Can probably be cached with functools.cache but it’s hard to imagine a cache being much faster than a join:

exc.add_note("\n".join(notes))
4 Likes

I’m used to add a note (just one - unlike the OP) quite often:

try:
    ...
except Exception as err:
    err.add_note(f"This error occurred while processing job: {job_id}")
    raise err

and would welcome a method similar to the suggested one:

try:
    ...
except Exception as err:
    raise err.with_note(f"This error occurred while processing job: {job_id}")
3 Likes

It may just so happen that one would like to report a discrepancy between the keys of a dictionary and a container, where the container should be a subset/superset of the keys. In that case, I personally use map to format each missing/extra key in a certain way and add them as notes to the exception. Something like:

raise ValueError('unknown keys:').with_notes(map('- key %d: %r'.__mod__, enumerate(extra_keys)))

would greatly simplify my code. This may be a niche pattern though, and Xitop’s suggestion makes sense.

That would be the same as some

raise ValueError('unknown keys:').with_note(
    "\n".join(string for string in map(
        '- key %d: %r'.__mod__,
        enumerate(extra_keys)
     ))
)

In my opinion only a with_note method should be needed, but I agree it would be a useful utility either way. The pattern of adding a note, then raising the exception should be common enough, so I don’t think a lot more reasoning should be required.