Поговорим об исключениях.
Всё нижеизложенное относится к Python 3.3, хотя отчасти справедливо и
для более ранних версий.
Для чего они применяются, наверное, все и так прекрасно знают: для
передачи сообщений об ошибках внутри программы.
Рассмотрим простейший пример: открытие файла. Если всё нормально —
open(filename, 'r') возвращает объект этого самого файла, с которым
можно делать всякие полезные вещи: читать из него данные и т.д.
Если файл не может быть открыт — выбрасывается исключение:
try:
    f = open(filename, 'r')
    try:
        print(f.read())
    finally:
        f.close()
except OSError as ex:
    print("Cannot process file", filename, ": Error is", ex)
Открываем файл и печатаем его содержимое.
Обратите внимание: файл нужно не только открыть но и закрыть после
использования. Исключение может выбросить open (например, если файла
нет на диске или нет прав на его чтение).
Если файл открыт — читаем его через f.read(). Этот вызов тоже может
выбросить исключение, но файл закрывать всё равно нужно.  Поэтому
необходим блок finally: f.close() должен быть вызван даже если
f.read() сломался. В этом месте удобней было бы воспользоваться
конструкцией with но мы же сейчас говорим об исключениях а не о
контекстных менеджерах, верно?
Исключения из обоих мест попадут в except OSError, где можно будет
что-то сделать с ошибкой.
Питон делает явный выбор в пользу исключений перед возвратом кода
 ошибки в своём ядре и стандартной библиотеке. Прикладному
 программисту стоит придерживаться этих же правил.
Введение закончено.  Теперь сконцентрируемся  на том что  происходит в
except.
Типы исключений
Все исключения составляют иерархию с простым наследованием. Вот
простой небольшой кусочек от довольно обширного набора исключений ядра
Питона:
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
Самый базовый класс — BaseException. Он и его простые потомки
(SystemExit, KeyboardInterrupt, GeneratorExit) не предназначены
для перехвата обыкновенным программистом — только Питон и редкие
библиотеки должны работать с этими типами. Нарушение правила ведет,
например, к тому что программу невозможно корректно завершить — что
совсем не хорошо.
Также не нужно перехватывать все исключения:
try:
    ...
except:
    ...
работает как
try:
    ...
except BaseException:
    ...
Всё, что может быть нужно программисту — это Exception и
унаследованные от него классы.
Вообще-то лучше ловить как можно более конкретные классы исключений.
Например, в случае с файлом это OSError или даже может быть
FileNotFoundError. Таким образом мы не перехватим AttributeError
или ValueError, которые в этом примере означали бы ошибку или
опечатку программиста.
Кстати, обратите внимание: StopIteration порожден от Exception а
GeneratorExit от BaseException. Подробности, почему сделано именно
так, можно найти в PEP 342.
Цепочки исключений
Прочитав предыдущую главку все прониклись необходимостью указывать
правильный класс исключений и пообещали никогда не использовать
BaseException.
Идем дальше. Следующий пример:
try:
    user = get_user_from_db(login)
except DBError as ex:
    raise UserNotFoundError(login) from ex
Получаем пользователя из базы данных чтобы что-то потом с ним
сделать. get_user_from_db может выбросить ошибку базы данных. Для
нас это скорее всего означает что такого пользователя нет. Но для
логики приложения полезней наш собственный тип UserNotFoundError с
указанием логина проблемного пользователя, а не обезличенная ошибка БД
— что мы и выбрасываем в обработчике исключения.
Проблема в том, что программисту часто хотелось бы знать, а почему это
пользователь не найден. Например, чтобы сохранить в логах для
дальнейшего разбирательства.
Для таких целей служит конструкция raise ... from ....
По PEP 3134 у объекта
исключения имеется несколько обязательных атрибутов.
В первую очередь это __traceback__, содержащий кусочек стека от
места возникновения исключения до места его обработки.
Затем — __context__. Если исключение было создано в ходе обработки
другого исключения (выброшено из except блока) — __context__
будет содержать то самое породившее исключение. Которое, в свою
очередь тоже может иметь установленный __context__. Этот атрибут
равен None если наше исключение — самое первое и не имеет
предшественников.
__context__ устанавливается автоматически.
В отличие от контекста __cause__ устанавливается только если
исключение было выброшено конструкцией raise ... from ... и равно
значению from.
Если исключение выбрасывалось простым raise ... то __cause__ будет
равно None в то время как __context__ всегда будет содержать
породившее исключение если оно существует.
Для вывода исключения со всей информацией служит набор функций из
модуля traceback, например traceback.print_exc().
И тут тоже есть проблема: печатается либо явная цепочка если есть
установленный __cause__ или неявная, тогда используется
__context__.
Иногда программисту может быть нужно отбросить породившие исключения
как не имеющие смысла при выводе traceback. Для этого появилась форма записи
raise exc from None
PEP 409 и PEP
415 рассказывают как это
работает:
У исключения всегда есть атрибут __supress_context__. По умолчанию
он равен False.
Конструкция raise ... from ... записывает from
в __cause__ и устанавливает __supress_context__ в True.
Тогда семейство функций traceback.print_exc() печатают цепочку если
явно указан (не равен None) __cause__ или есть __context__ и при
этом __supress_context__ равен False.
Изложение получилось несколько длинным, но сократить текст без потери
смысла у меня не вышло.
Семейство OSError
Последняя проблема о которой хотелось бы рассказать — это типы
исключений порожденные вызовами операционной системы.
До Python 3.3 существовало много разных типов таких исключений:
os.error, socket.error, IOError, WindowsError, select.error
и т.д.
Это приводило к тому, что приходилось указывать несколько типов
обрабатываемых исключений одновременно:
try:
    do_something()
except (os.error, IOError) as ex:
    pass
Ситуация на самом деле была еще хуже: очень легко забыть указать еще
одно нужное исключение, которое может внезапно прилететь. Дело в том
что исключения операционной системы часто никак не проявляют себя при
разработке. На машине программиста всё работает отлично и он не
подозревает о возможных проблемах. Как только программа выходит в
production пользователь вдруг ловит что-то неожиданное и программа
аварийно завершается. Все опечалены.
Проблема решена в PEP 3151:
весь этот зоопарк теперь
является псевдонимами для OSError. Т.е. пишите OSError и не
ошибетесь (прочие имена оставлены для обратной совместимости и
облегчения портирования кода на новую версию).
Давайте рассмотрим ещё один аспект исключений, порожденных
операционной системой.
У OSError есть атрибут errno, который содержит код ошибки (список
всех возможных символьных констант для ошибок можно посмотреть в модуле
errno).
Открываем файл, получаем OSError в ответ. Раньше мы должны были
анализировать ex.errno чтобы понять, отчего произошла ошибка: может
файла нет на диске, а может нет прав на запись — это разные коды
ошибок (ENOENT если файла нет и EACCES или EPERM если нет прав).
Приходилось строить конструкцию вроде следующей:
try:
    f = open(filename)
except OSError as ex:
    if ex.errno == errno.ENOENT:
       handle_file_not_found(filename)
    elif ex.errno in (errno.EACCES, errno.EPERM):
       handle_no_perm(filename)
    else:
       raise  # обязательно выбрасывать не обработанные коды ошибки
Теперь иерархия расширилась. Привожу полный список наследников OSError:
OSError
 +-- BlockingIOError
 +-- ChildProcessError
 +-- ConnectionError
 |    +-- BrokenPipeError
 |    +-- ConnectionAbortedError
 |    +-- ConnectionRefusedError
 |    +-- ConnectionResetError
 +-- FileExistsError
 +-- FileNotFoundError
 +-- InterruptedError
 +-- IsADirectoryError
 +-- NotADirectoryError
 +-- PermissionError
 +-- ProcessLookupError
 +-- TimeoutError
Наш пример можем переписать как:
try:
    f = open(filename)
except FileNotFound as ex:
    handle_file_not_found(filename)
except PermissionError as ex:
    handle_no_perm(filename)
Гораздо проще и понятней, правда? И меньше мест, где программист может
ошибиться.
Заключение
Переходите на Python 3.3, если можете. Он хороший и облегчает жизнь.
Новые плюшки в вопросе, касающемся исключений, я показал.
Если использовать новый питон не позволяют обстоятельства — пишите на
чём есть, но помните как правильно это делать.