Instance/subclass check for `except` clauses

How does except work?

I thought, naively, that it would use isinstance or issubclass to know whether to enter the except clause or not, and so tried overriding __instancecheck__ and __subclasscheck__ in the metaclass of an exception, but they are not triggered when I’m catching it.

class MetaError(type):
    def __new__(metacls, name, bases, namespace, **kwargs):
        return super().__new__(metacls, name, bases, namespace)

    def __init__(cls, name, bases, namespace, new):
        cls.new = new
        super().__init__(name, bases, namespace)

    def __instancecheck__(self, instance):
        if isinstance(instance, self.new):
            print(f"DEPRECATED, catch {self.new!r} instead")
            return True
        return super().__instancecheck__(instance)

    def __subclasscheck__(self, subclass):
        if issubclass(subclass, self.new):
            print(f"DEPRECATED, catch {self.new!r} instead")
            return True
        return super().__subclasscheck__(subclass)


class New(Exception):
    ...


class Old(Exception, metaclass=MetaError, new=New):
    ...


print(isinstance(New(), Old))
print(issubclass(New, Old))

try:
    raise New()
except Old as error:
    print("Caught Old")
except New as error:
    print("Caught New")

Output:

DEPRECATED, catch <class '__main__.New'> instead
True
DEPRECATED, catch <class '__main__.New'> instead
True
Caught New

I was expecting to see “Caught Old” at the end :confused:

It is checked in C with this API:

which in turn depends on PyType_IsSubtype (Type Objects — Python 3.13.0 documentation), which says doesn’t call __subclasscheck__.

1 Like

Thanks!

OK. For the context, I’m trying to see how I can detect deprecated usage in downstream code (at runtime, to emit deprecation warnings), if I change the exception raised by a callable. But this would only work if I have control over the exception classes anyway (to set the metaclass, etc.), so not with builtin exceptions like ValueError, etc.

In short, I don’t think what I’m trying to achieve is possible without static analysis :thinking:

Thanks again @jeff5 :slightly_smiling_face:

If I had to summarize, I’d say I want to “hook” into what happens when an exception is caught. Is that possible?

I think what you can do is simply hook into the constructor of deprecated exception classes.
I’m not sure how you could possibly detect built-in exception handling as deprecated.

Hook into constructor, because why would just solely constructing the exception object not be deprecated in case of a deprecated exception class? In 99% cases you construct the exception object just before raising it. In 1% cases you construct it to raise it at some point in the future.

Basically, I want to raise a new exception, while users may still be trying to catch the old one, and I want that to still work, but to emit a deprecation warning to indicate they should catch the new exception instead.

The old exception might not be deprecated itself: it could still be used elsewhere. What is deprecated is catching a specific exception from a specific callable, because this callable now raises another one :slightly_smiling_face:

To give an example, myfunc would change from raising ValueError into raising RuntimeError, and I want to emit a deprecation warning when downstream code tries to catch ValueError.

Oh, I think I see now. Nevermind my messages above. Makes sense.

1 Like

And it is worth noting that you emit deprecation warnings only if it is necessary (if the caught exception is not a base of both A and B, where A is the deprecated exception class and B is the new exception class).

1 Like

The only way I think of possibly achieving that is very hackish and relies strongly on fragile assumptions and frame inspection.

1 Like

Right. Catching a common base shouldn’t emit the deprecation warning.

Even static analysis may not be easy to get right here.
You could even perform it at runtime, by finding the code of the caller and inspecting the AST of it astor.code_to_ast(caller) (see the astor package).
But even then, imagine a scenario where the exception raised by your package is caught by a context manager such as suppress(). Inspecting try/except wouldn’t work here. Moreover, a function/module where a deprecated error is caught does not have to reference the error, as suppress() or other context managers can reference it out of the scope of the use (imagine there is a context manager with except Old: somewhere, reused in the function possibly statically examined for the use of Old), so inspecting co_names of the caller code object would be insufficient.

What I think this should lead to is maybe a proposal of ability to catch the catcher red-handed via a simple hook that would be called after sys.exc_info is available to be inspected.

1 Like

…or you may simply end up emiting a deprecation warning in module-level __getattr__ of somewhere in your package as soon as some package other than yours imports the deprecated exception class?

Definitely. I can imagine hacks with inspecting code (not always available btw) or frames (super fragile as you mentioned). But a proper way of hooking into except thing would be much more robust, because even if I’m using built-in exceptions, I can create a custom one that inherits from the built-in ones, and raise that during the deprecation period.

class PreferRuntimeErrorOverValueError(ValueError, RuntimeError):
    def __exceptcheck__(self, types):
        # some logic that emits or not the deprecation warning

Yeah but that only works if I raise custom exceptions, and if the first exception is completely deprecated, not just for the specific callable.

Yeah. :confused:
Uh!!

Btw, regarding the original question:

Oh, interesting, so that means my __instancecheck__ and __subclasscheck__ methods would work with contextlib.suppress, right?

So, should I create a new issue on the bugtracker, mentioning [doc] clarify that except does not match virtual subclasses of the specified exception type · Issue #56238 · python/cpython · GitHub, showing this “new” use-case? Or should I open a new Idea thread here?