Как устроена иерархия исключений в Python?
Что проверяют
Понимаете ли вы, почему except Exception и голый except: ведут себя по-разному и в каком порядке писать обработчики. Следом часто спрашивают, от чего наследовать своё исключение и что делает raise ... from.
Как отвечать
Нарисуйте верх дерева:
BaseException: корень. Напрямую от него наследуютсяSystemExit,KeyboardInterrupt,GeneratorExit,Exception, а с 3.11 ещёBaseExceptionGroup;Exception: база для обычных ошибок и для ваших собственных;ArithmeticError:ZeroDivisionError,OverflowError;LookupError:KeyErrorиIndexError, их удобно ловить одним обработчиком;OSError:FileNotFoundError,PermissionError,ConnectionError,TimeoutError.IOErrorиEnvironmentErrorсейчас просто его псевдонимы;- прочие:
ValueError,TypeError,AttributeError,ImportErrorс потомкомModuleNotFoundError,RuntimeErrorсRecursionErrorиNotImplementedError.
Почему SystemExit и KeyboardInterrupt вынесены из Exception: чтобы except Exception не глотал sys.exit() и Ctrl+C. По той же причине asyncio.CancelledError с Python 3.8 наследуется от BaseException: отмена задачи не должна теряться в общем обработчике.
Порядок: срабатывает первый подходящий except, поэтому конкретные классы идут раньше базовых. Своё исключение наследуйте от Exception, а в библиотеке заведите общий базовый класс (class PaymentError(Exception)) и потомков под частные случаи, чтобы вызывающий код ловил на нужном уровне.
Цепочки: raise NewError(...) from e кладёт причину в __cause__, неявный контекст сохраняется в __context__, а from None его скрывает. С 3.11 есть ExceptionGroup и except* для нескольких ошибок сразу, например из asyncio.TaskGroup, и add_note() для пояснений.
Где спотыкаются
Упоминают StandardError. В Python 3 его нет вовсе, это класс из Python 2.
Пишут голый except: или except BaseException без повторного raise. Такой код глотает Ctrl+C и мешает процессу завершиться.
Ставят except Exception перед except ValueError. Второй обработчик становится мёртвым кодом, и Pylint об этом предупреждает.