An explicit check of the callback's ability to handle parameters is about the best you're going to be able to do. Python may be loosie-goosie in its duck typing, but it will complain and raise a
TypeError exception if you feed a function the wrong number of parameters. No ifs, ands, or buts about that.
You have existing functions in the field that you don't feel you can change--and probably for good reason. So having an inspection that asks, "what can this callback function accept?" or "what does this callback function expect?"--it may be inelegant, but that's what's available to you.
The other choices involve:
Having some form of object-relative state (such as an additional instance value or method) or even global state (whether a true global variable, a class variable, a singleton reporting object, or whathaveyou) which callbacks can reference to get the extended information you're now offering them. This is how C, Unix, and many other codebases handle exceptional and additional information. It is, unfortunately, not especially elegant--and it can be quite problematic/unworkable for multithreaded apps.
On the off chance that your
data parameter is an extensible type, you might be able to add fields to it. If it's a
dict or similar structure you're golden, as long as the callbacks play by duck typing rules and don't strictly check that they got only the fields back they expected. This is a trick that often works in dynamic languages, even though it would fall flat on its face in statically typed languages/data passing environments. But, you have to get lucky to have this work. If
data is a more static type, you're back to choice 1 or your inspection-based approach ("choice 0").
As an aside, your code will run fine in Python 2. But Python 3 changes the place the code is stored. To encompass both your current Python 2 and future moves to Python 3, here's a shim that works in both:
_PY2 = sys.version_info == 2
code = f.func_code if _PY2 else f.__code__
if arg_count(self._callback) > 1: