Исключение ловят не там, где оно возникло, а там где его удобно обработать. Исключение само будет всплывать от функции к функции пока не встретит подходящий обработчик.


Пример 1

Плохо:


def divide(num1, num2):
    result = None
    try:
        result = num1 / num2
    except ZeroDivisionError as e:
        logging.error(f'ZeroDivisionError: {e}')
    finally:
        return result


def main():
    num1 = 1
    num2 = 0
    result = divide(num1, num2)
    print(result)

Хорошо:


def divide(num1, num2):
    result = num1 / num2
    return result


def main():
    num1 = ...
    num2 = ...
    try:
        result = divide(num1, num2)
    except ZeroDivisionError as e:
        logging.error(f'ZeroDivisionError: {e}')
    finally:
        print result


Пример 2

Функция, которая поднимает исключение, должна только сигнализировать об ошибке, а не решать, как с ней поступать.

Плохо:


class EmptyLessonError(Exception):
    pass


def validate_lesson(raw_lesson):
    try:
        if raw_lesson == '':
            raise EmptyLessonError('Урок не содержит материалов!')
        ...
    except EmptyLessonError as e:
        logging.warning(f'EmptyLessonError: {e}')
    except ...
    ...


def main():
    raw_lesson = ...
    lesson = validate_lesson(raw_lesson)
    ...

Хорошо:


class EmptyLessonError(Exception):
    pass


def validate_lesson(raw_lesson):
    if raw_lesson == '':
        raise EmptyLessonError('Урок не содержит материалов!')
    ...


def main():
    raw_lesson = ...
    try:
        lesson = validate_lesson(raw_lesson)
    except EmptyLessonError as e:
        logging.warning(f'EmptyLessonError: {e}')
    except ...