Исключения

Какие блоки для обработки исключений есть в Python?

подробнее

Существует 4 блока:

  • try - выполняется всегда, в него помещается код, который может отдать исключение
  • except - выполняется только в том случае если в блоке try возникло исключение, которое ловит этот except
  • else - выполняется в том случае если try выполнился без исключений
  • finally - выполняется всегда в конце.

Можно ли использовать несколько except?

подробнее

Можно. Они будут обрабатываться в порядке очерёдности. (В Python блоки except проверяются сверху вниз до первого совпадения.)
«Узкие» (специфичные) исключения должны находиться выше, чем «широкие» (базовые). Пример: Если первым поставить except Exception, то до except ValueError код никогда не дойдет.

Все исключения унаследованы от BaseException:

BaseException
 ├── Exception
 │   ├── ArithmeticError
 │   │   ├── ZeroDivisionError
 │   │   └── OverflowError
 │   ├── ValueError
 │   ├── IndexError
 │   ├── KeyError
 │   └── TypeError
 ├── SystemExit
 └── KeyboardInterrupt

Раздельные блоки (разная логика для разных ошибок):

try:
    raise ValueError(1)
except ValueError:
    print('ValueError')  # выполнится этот блок
except Exception:
    print('Exception')

Группировка в кортеж (одна логика для нескольких ошибок):

try:
    raise IOError(1)
except (ValueError, IOError) as e:
    print('ValueError or IOError')  # выполнится этот блок
except Exception:
    print('Exception')

В чём разница между except и except Exception?

подробнее

except без Exception ловит BaseException.
BaseException - это базовый класс для всех ошибок. Перехватывая BaseException будет перехватываться вообще всё, включая системные прерывания (Ctrl+C).
Всегда рекомендуется использовать except Exception.

Как вызвать исключение? Как рерайзнуть исключение?

подробнее

Как вызвать (возбудить) исключение

Используется синтаксис raise ИмяИсключения("сообщение").

Пример:

if age < 0:
    raise ValueError("Возраст не может быть отрицательным")

Как рерайзнуть (пробросить выше)

  • Чистый raise (рекомендуемый)
    Сохраняется весь исходный Traceback (стек вызовов), видно, где ошибка возникла изначально.

      try:
          1 / 0
      except ZeroDivisionError:
          print("Логируем ошибку...")
          raise  # Пробрасывает ТУ ЖЕ САМУЮ ошибку дальше
    
  • raise e

      except ZeroDivisionError as e:
          raise e # Traceback начнется с этой строки, оригинал потеряется
    
  • Цепочка исключений (from)
    Показывает, что одна ошибка стала причиной другой (в консоли будет написано: The above exception was the direct cause…).

      except ZeroDivisionError as e:
          raise RuntimeError("Ошибка сервера") from e
    

Зачем нужны классы BaseExceptionGroup и ExceptionGroup?

подробнее

Эти классы появились в Python 3.11 для обработки ситуаций, когда одновременно возникает несколько независимых ошибок.

Раньше raise мог выбросить только одну ошибку. Но в современном коде (особенно в асинхронности или при работе с потоками) может упасть сразу несколько задач.
ExceptionGroup позволяет упаковать список этих ошибок в один «контейнер» и пробросить его дальше.

Разница:

  • ExceptionGroup: Для обычных ошибок (наследуется от Exception). Включает ValueError, TypeError и т.д.
  • BaseExceptionGroup: Более «тяжелый» вариант (наследуется от BaseException). Включает системные ошибки вроде KeyboardInterrupt (Ctrl+C) или SystemExit.

В чём разница между except и except*?

подробнее

except - для одиночных исключений. Нашел совпадение — остановился.
except* - для групп исключений. Проверяет все блоки и «выкусывает» из группы ошибки соответствующих типов, пока группа не опустеет.

Как это работает на практике:

  • except:
    Если вылетит и ValueError, и TypeError (хотя обычный код так не умеет), except поймает только то, что прописано выше по коду. Остальное проигнорирует.
  • except*:
    Если в ExceptionGroup лежат три ошибки (две ValueError и одна KeyError), то:
    Блок except* ValueError заберет обе ошибки этого типа.
    Блок except* KeyError выполнится следом и заберет свою.
    Если в группе осталось что-то не пойманное, оно полетит дальше (re-raise) автоматически.

Пример:

try:
    raise ExceptionGroup("Проблемы в сети", [
        ValueError("Неверный формат"),
        TypeError("Не тот тип")
    ])
except* ValueError as eg:
    print(f"Обработали только ValueError: {eg.exceptions}")
except* TypeError:
    print("Обработали только TypeError")

Как создать свой тип исключения?

подробнее

Создать свое исключение - значит создать класс, который наследуется от встроенного класса Exception.

Примеры:

# Простое исключение
class MyError(Exception):
    pass

raise MyError("Что-то пошло не так")
# Исключение с доп. логикой
class ValidationError(Exception):
    def __init__(self, message, code):
        super().__init__(message)  # Передаем сообщение в базовый класс
        self.code = code           # Добавляем свое поле

raise ValidationError("Неверный пароль", 403)