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:
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_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.