__cause__ или функции raise ... from ....  Это позволяет отслеживать всю цепочку событий, приведших к ошибке.
  
try:
    # ... что-то, что может вызвать ValueError
    raise ValueError("Ошибка преобразования типа")
except ValueError as e:
    try:
        # ... обработка ValueError и возможно возникновение IOError
        raise IOError("Ошибка ввода/вывода")
    except IOError as e2:
        raise RuntimeError("Критическая ошибка") from e2 # Связываем с IOError
except IOError as e3:
    raise RuntimeError("Критическая ошибка (IOError)") from e3 # Связываем с IOError
  RuntimeError, атрибут __cause__ содержит оригинальный IOError (или ValueError, в зависимости от того, где была ошибка), позволяя провести детальный анализ причин ошибки. Использование raise ... from None отключает цепочки исключений.
Для создания исключений с несколькими уровнями вложенности в Python, можно использовать механизм, когда одно исключение вызывает другое, сохраняя информацию о причине ошибки на каждом уровне. Это позволяет получить более подробную картину произошедшего и облегчает отладку сложных ситуаций.
Основной подход заключается в использовании аргумента from в инструкции raise.  С помощью raise ExceptionA from ExceptionB мы указываем, что ExceptionA вызвано (или является прямым следствием) ExceptionB.
Пример:
class DatabaseError(Exception):
    """Общий класс для ошибок базы данных."""
    pass
class ConnectionError(DatabaseError):
    """Ошибка подключения к базе данных."""
    pass
class QueryError(DatabaseError):
    """Ошибка выполнения запроса к базе данных."""
    pass
def connect_to_database(host, port):
    """Попытка подключения к базе данных."""
    try:
        # Имитация неудачного подключения
        raise OSError("Ошибка сети: невозможно подключиться к хосту")
    except OSError as e:
        raise ConnectionError("Не удалось установить соединение с базой данных") from e
def execute_query(connection, query):
    """Попытка выполнить запрос."""
    try:
        # Имитация ошибки в запросе
        raise ValueError("Некорректный SQL запрос")
    except ValueError as e:
        raise QueryError("Ошибка при выполнении запроса") from e
def process_data(host, port, query):
    """Основная функция обработки данных."""
    try:
        connection = connect_to_database(host, port)
        execute_query(connection, query)
    except DatabaseError as e:
        print(f"Произошла ошибка базы данных: {e}")
        if e.__cause__:
            print(f"  Вызвано исключением: {e.__cause__}") #Выводит исходную ошибку
    except Exception as e:
        print(f"Непредвиденная ошибка: {e}")
# Пример использования
process_data("localhost", 5432, "SELECT * FROM invalid_table")
  В данном примере:
DatabaseError -> ConnectionError, QueryError.connect_to_database ловит OSError и генерирует ConnectionError, указывая, что причиной была сетевая ошибка.execute_query ловит ValueError и генерирует QueryError, указывая, что причиной была ошибка в запросе.process_data обрабатывает исключение DatabaseError и, если у него есть причина (__cause__), выводит информацию о ней.Преимущества такого подхода:
DatabaseError и повторить операцию, если причиной была временная сетевая проблема (ConnectionError).Важно:  При создании вложенных исключений следите за тем, чтобы каждое новое исключение предоставляло дополнительную информацию, а не просто повторяло предыдущее.  Аргумент from следует использовать только тогда, когда новое исключение действительно вызвано предыдущим.
Альтернативные подходы (встречаются реже):
В заключение, создание исключений с несколькими уровнями вложенности с помощью аргумента from является мощным инструментом для обработки сложных ошибок в Python. Он обеспечивает сохранение цепочки причин, упрощает отладку и делает сообщения об ошибках более информативными.