Exception tutorial confusion

In the error-tutorial (8. Errors and Exceptions — Python 3.13.5 documentation) the following example is listed:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Followed by this text:

Note that if the except clauses were reversed (with except B first), it would have printed B, B, B — the first matching except clause is triggered.

However, the true reason for the reversed order is that both C and D inherit from B, so this is an inheritance issue not a matching order. I found that quite confusing.

It’s both. The example is trying to explain that if there is an inheritance relationship between the exception classes, the order of the except clauses matters (and you should place them with the most-derived class first).

Do you want to suggest an improvement to the wording of the example that would have clarified this for you?

1 Like

Thank you very much for that kind answer! It took me quite a while to understand the issue together with the caption above. It is a clever example.
Less elegant, but easier to grasp would probably be something like the following:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        # Case B: B is not a subclass of D, next exception
        # Case C: C is not a subclass of D, next exception
        # Case D: match!
        print("D")
    except C:
        # Case B: B is not a subclass of C, next exception
        # Case C: match! 
        # Case D: exception is not reached (and not a subclass of C)
        print("C")
    except B:
        # Case B: match!
        # Case C: match (subclass of B), but exception is not reached 
        # Case D: match (subclass of B), but exception is not reached       
        print("B")
2 Likes

Needs an “is not reached” for D.

2 Likes

In general we value conciseness. We don’t need # Case D: match! because that’s the intended behavior.

I think using three classes for the example is overkill – we can shorten the example by nearly 1/3rd if we just use classes B and C. We then get:

class B(Exception):
    pass

class C(B):
    pass

for cls in [B, C]:
    try:
        raise cls()
    except C:
        # Matches C but not B.
        print("C")
    except B:
        # Matches B; not reached for C.
        print("B")

And I would explicitly write out the reversed example:

for cls in [B, C]:
    try:
        raise cls()
    except B:
        # Matches B and C both.
        print("B")
    except C:
        # Not reached (the previous clause ate B and C)
        print("C")
5 Likes

comment got edited, thanks!