Исключения: try/except/else/finally для файлов и ввода, raise, свои сообщения

Вспомните: в главе 1 перед открытием файла вы проверяли path.exists(), чтобы программа не упала на отсутствующем файле. Но файл может быть на месте и всё равно не прочитаться: кодировка не та, в ячейке суммы вместо числа — «сто», человек ввёл пустую строку. Тогда программа обрывалась на полуслове с красным текстом ошибки. В этом уроке учим её переживать такие случаи.

🎯 После урока сможете: прочитать текст ошибки и найти её тип, перехватить конкретную ошибку через try/except, пропустить битую строку таблицы и продолжить работу, выполнить код только при успехе (else) и всегда (finally), а в своей функции остановить работу с понятным сообщением через raise.

Что такое исключение и как читать текст ошибки

Когда Python не может выполнить строку, он создаёт исключение — объект с типом ошибки и сообщением — и останавливает программу. На экран выводится отчёт (его называют traceback): где упало и почему. Читать его надо снизу вверх: последняя строка — тип и сообщение, строки выше — путь к месту ошибки.

summa = int("12,5")

Вывод:

Traceback (most recent call last):
  File "rashody.py", line 1, in <module>
    summa = int("12,5")
ValueError: invalid literal for int() with base 10: '12,5'

Здесь тип — ValueError (значение не подходит: строку с запятой нельзя превратить в целое), сообщение — после двоеточия. Типы, которые встретятся в работе с файлами и вводом чаще всего:

  • ValueError — значение неподходящее: int("сто"), int(""), float("12р");
  • FileNotFoundError — файла нет: open("nety.txt");
  • UnicodeDecodeError — байты не читаются в указанной кодировке: файл в cp1251 открыли как utf-8;
  • ZeroDivisionError — деление на ноль;
  • KeyError — нет такого ключа в словаре: settings["шрифт"], а ключа нет;
  • IndexError — нет элемента с таким номером в списке.
Текст ошибки из четырёх строк на тёмном фоне терминала. Строка «Traceback (most recent call last):» подписана «начало отчёта». Строка «File "rashody.py", line 1» подписана «файл и номер строки». Строка кода «summa = int("12,5")» подписана «что выполнялось». Последняя строка «ValueError: invalid literal for int() with base 10: '12,5'» выделена: слово ValueError подписано «тип ошибки», текст после двоеточия — «сообщение». Стрелка снизу вверх с надписью «читать отсюда».

Проверьте себя

Задание. Какой тип ошибки выдаст программа? Введите имя исключения, как оно написано в последней строке отчёта.

Исходный код для этого задания:

amount = "сто"
total = 450 + int(amount)
print(total)

Подсказка: Что не так: строка, которую превращают в число, или сама операция сложения?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Файла otchet_sentyabr.txt в папке нет. Какой тип ошибки выдаст программа?

Исходный код для этого задания:

with open("otchet_sentyabr.txt", encoding="utf-8") as f:
    text = f.read()
АValueError
БFileNotFoundError
ВUnicodeDecodeError
ГKeyError

Подсказка: Откройте список частых ошибок в начале урока: какая из них относится к открытию файла, которого нет?

Онлайн-проверка ответа появится позже

try/except: перехватываем конкретную ошибку

Опасный код кладут в блок try, а в except пишут, что делать, если случилась ошибка указанного типа. Программа не падает, а идёт дальше. Главный приём для таблицы расходов — пропустить битую ячейку и считать остальное:

sums = ["450", "380", "сто", "620"]
total = 0
for s in sums:
    try:
        total += int(s)
    except ValueError:
        print("Не число, пропускаю:", s)
print("Итого:", total)

Вывод:

Не число, пропускаю: сто
Итого: 1450

Строка total += int(s) для «сто» не выполнилась: Python прыгнул в except, напечатал сообщение и перешёл к следующему элементу цикла.

Чтобы увидеть текст самой ошибки, дайте ей имя через as: внутри except переменная e хранит исключение, а print(e) печатает его сообщение.

settings = {"тема": "тёмная"}
try:
    size = settings["шрифт"]
except KeyError as e:
    print("В настройках нет ключа", e)
    size = 12
print("Шрифт:", size)

Вывод:

В настройках нет ключа 'шрифт'
Шрифт: 12

Пишите в except конкретный тип. Голый except: или except Exception: ловит всё подряд, включая опечатку в имени переменной, — и программа молча делает не то, а вы не узнаете почему.

Проверьте себя

Задание. Что программа напечатает последней строкой? Введите число. Обратите внимание: в «3OO» — буквы O, а не нули.

Исходный код для этого задания:

total = 0
for s in ["1200", "", "350", "3OO", "150"]:
    try:
        total += int(s)
    except ValueError:
        print("пропуск")
print(total)

Подсказка: Пройдите список по одному элементу: какие строки int() превращает в число, а какие уходят в except?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Файл zametki.txt в папке есть, но сохранён в старом Блокноте в кодировке cp1251. Программа должна в этом случае перечитать его в cp1251, а вместо этого падает. Какой точный тип ошибки стоит в последней строке отчёта при падении? Его и нужно написать в except вместо KeyError. Введите имя исключения.

Исходный код для этого задания:

from pathlib import Path

path = Path("zametki.txt")
try:
    text = path.read_text(encoding="utf-8")
except KeyError:
    text = path.read_text(encoding="cp1251")
print("Символов:", len(text))

Подсказка: Файл на месте, значит дело не в его отсутствии. Что не получается при чтении: байты или ключ словаря? Сверьтесь со списком частых ошибок урока.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Несколько except: разная реакция на разные ошибки

После одного try можно поставить несколько except. Python проверяет их сверху вниз и выполняет первый подходящий. Пример — делим счёт на компанию: строка вида «сумма/человек».

def share(line):
    total, people = line.split("/")
    try:
        return int(total) // int(people)
    except ValueError:
        print("Не число:", line)
        return 0
    except ZeroDivisionError:
        print("Некому платить:", line)
        return 0

print(share("3000/4"))
print(share("3000/0"))
print(share("три/2"))

Вывод:

750
Некому платить: 3000/0
0
Не число: три/2
0

Если реакция на разные ошибки одинаковая, перечислите типы в скобках: except (ValueError, KeyError):.

rows = [{"сумма": "450"}, {"итог": "300"}, {"сумма": "12р"}, {"сумма": "80"}]
total = 0
bad = 0
for row in rows:
    try:
        total += int(row["сумма"])
    except (ValueError, KeyError):
        bad += 1
print(total, bad)

Вывод:

530 2

Порядок важен. Если первым стоит except Exception, он перехватит и ValueError, и ZeroDivisionError, и до более точных веток дело не дойдёт. Точные типы — выше, общий — в самом низу или не нужен вовсе.

Проверьте себя

Задание. Что выведет программа? Введите число.

Исходный код для этого задания:

def share(line):
    total, people = line.split("/")
    try:
        return int(total) // int(people)
    except ValueError:
        return -1
    except ZeroDivisionError:
        return 0

result = 0
for line in ["1000/4", "900/0", "x/3", "600/3"]:
    result += share(line)
print(result)

Подсказка: Посчитайте share() для каждой строки отдельно: какая ветка сработает — обычный return, except ValueError или except ZeroDivisionError? Потом сложите.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет программа?

Исходный код для этого задания:

try:
    n = int("двадцать")
except Exception:
    print("Что-то пошло не так")
except ValueError:
    print("Введите число цифрами")
АЧто-то пошло не так
БВведите число цифрами
ВОбе строки по очереди
ГПрограмма упадёт с ValueError

Подсказка: Ветки except проверяются сверху вниз, выполняется первая подходящая. Подходит ли ValueError под Exception?

Онлайн-проверка ответа появится позже

Задание. Сколько позиций заказа программа посчитает проблемными? Введите число, которое она выведет.

Исходный код для этого задания:

prices = {"хлеб": "60", "сыр": "сто", "чай": "250"}
order = ["хлеб", "сыр", "кофе", "чай"]
bad = 0
for item in order:
    try:
        int(prices[item])
    except (ValueError, KeyError):
        bad += 1
print(bad)

Подсказка: Для каждой позиции спросите: есть ли такой ключ в словаре? если есть — превращается ли значение в число?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

else и finally: при успехе и всегда

У try есть ещё две необязательные ветки:

  • else — выполняется, только если в try ошибки не было. Сюда кладут работу с результатом: так в try остаётся лишь опасная строка, и вы не перехватите чужую ошибку по случайности;
  • finally — выполняется всегда: была ошибка или нет. Для завершающих действий: закрыть, записать в журнал, сообщить «готово».
for s in ["7", "x"]:
    try:
        n = int(s)
    except ValueError:
        print(s, "- не число")
    else:
        print(s, "- число, удвоим:", n * 2)
    finally:
        print(s, "- проверено")

Вывод:

7 - число, удвоим: 14
7 - проверено
x - не число
x - проверено

Для «7» сработали else и finally, для «x» — except и finally. Файлы, открытые через with, закрываются сами; finally нужен для остального — счётчиков, журнала, итогового сообщения.

Схема потока выполнения. Сверху блок try. От него две стрелки: зелёная «ошибки нет» ведёт к блоку else, красная «ошибка нужного типа» — к блоку except. Обе стрелки от else и от except сходятся в блоке finally внизу, подпись «выполняется всегда». От finally стрелка к «код после try». Сбоку серая пунктирная стрелка от try: «ошибка другого типа» — через finally к «программа падает».

Проверьте себя

Задание. Что выведет программа? Введите строку из латинских букв O, E и F без пробелов, в верхнем регистре.

Исходный код для этого задания:

for s in ["7", "x", "12"]:
    try:
        n = int(s)
    except ValueError:
        print("E", end="")
    else:
        print("O", end="")
    finally:
        print("F", end="")

Подсказка: print(…, end=»») печатает без перехода на новую строку. Для каждого s решите: какая ветка — except или else — и выполняется ли finally.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа читает число из строки через int(), а затем делает с числом длинные расчёты. Куда положить расчёты, чтобы они выполнялись только при удачном int() и чтобы ошибки внутри расчётов не перехватывались тем же except ValueError?

Ав try, сразу после int()
Бв except
Вв else
Гв finally

Подсказка: Какая ветка выполняется только тогда, когда в try не было исключения?

Онлайн-проверка ответа появится позже

raise: останавливаем работу с понятным сообщением

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

def check_amount(amount):
    if amount <= 0:
        raise ValueError(f"сумма должна быть больше нуля, а пришло {amount}")
    return amount

print(check_amount(450))
try:
    check_amount(-300)
except ValueError as e:
    print("Ошибка:", e)

Вывод:

450
Ошибка: сумма должна быть больше нуля, а пришло -300

raise прерывает функцию сразу: return после него не выполняется. Исключение уходит к тому, кто функцию вызвал, и дальше вверх — пока его не поймает except или программа не упадёт с вашим сообщением. Тип берите подходящий по смыслу: неверное значение — ValueError, нет файла — FileNotFoundError.

Удобный приём — проверить всё в одной функции-разборщике и ловить одну ошибку снаружи:

def parse_amount(text):
    text = text.strip().replace(",", ".")
    amount = float(text)
    if amount <= 0:
        raise ValueError(f"не положительная сумма: {text}")
    return amount

for s in ["250,50", " 99 ", "-40", "бесплатно"]:
    try:
        print(parse_amount(s))
    except ValueError as e:
        print("Пропуск:", e)

Вывод:

250.5
99.0
Пропуск: не положительная сумма: -40
Пропуск: could not convert string to float: 'бесплатно'

Обратите внимание: ошибку float("бесплатно") Python создал сам, а ошибку для «-40» — ваш raise. Снаружи оба случая ловит один except ValueError.

Две рамки одна под другой. Верхняя — вызывающий код: цикл for и блок try с вызовом check_amount(-300), ниже except ValueError as e и print("Ошибка:", e). Нижняя рамка — функция check_amount: if amount <= 0 и строка raise ValueError("сумма должна быть больше нуля..."). Красная стрелка от raise поднимается вверх в except вызывающего кода, строка return amount перечёркнута серым. Справа итог: «Ошибка: сумма должна быть больше нуля, а пришло -300».

Проверьте себя

Задание. Что выведет программа? Введите строку вывода целиком.

Исходный код для этого задания:

def check_amount(amount):
    if amount <= 0:
        raise ValueError(f"сумма должна быть больше нуля, а пришло {amount}")
    return amount

try:
    check_amount(0)
except ValueError as e:
    print("Ошибка:", e)

Подсказка: Проходит ли 0 проверку amount <= 0? Если raise сработал, print(e) печатает текст, который передали в ValueError(…).

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Допишите пропуск: функция должна прервать работу с сообщением, если размер шрифта вне диапазона. Введите одно слово, которое стоит на месте ____.

Исходный код для этого задания:

def set_font(size):
    if size < 8 or size > 72:
        ____ ValueError("размер шрифта от 8 до 72")
    return size

Подсказка: Какой командой в Python создают своё исключение?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Что выведет программа? Введите два числа через пробел, как в выводе.

Исходный код для этого задания:

def parse_amount(text):
    text = text.strip().replace(",", ".")
    amount = float(text)
    if amount <= 0:
        raise ValueError("не положительная сумма")
    return amount

total = 0
skipped = 0
for s in ["120,5", "-15", "0", " 80 ", "12р", "9,5"]:
    try:
        total += parse_amount(s)
    except ValueError:
        skipped += 1
print(total, skipped)

Подсказка: Для каждой строки: что сделают strip и replace, превратит ли float её в число, пропустит ли проверка amount <= 0. Сложите только прошедшие.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Надёжное чтение файлов и ввода

Соберём приёмы для двух частых случаев из главы 1. Первый — файл, про который неизвестно, в какой он кодировке. Пробуем utf-8, при UnicodeDecodeError — cp1251 (старые файлы Windows):

def read_any(data):
    try:
        return data.decode("utf-8"), "utf-8"
    except UnicodeDecodeError:
        return data.decode("cp1251"), "cp1251"

print(read_any("Отчёт".encode("utf-8")))
print(read_any("Отчёт".encode("cp1251")))

Вывод:

('Отчёт', 'utf-8')
('Отчёт', 'cp1251')

С файлом на диске то же самое: в try — path.read_text(encoding="utf-8"), в except UnicodeDecodeError — повтор с encoding="cp1251", а отдельный except FileNotFoundError заменяет прежнюю проверку exists().

from pathlib import Path

def read_text_file(path):
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="cp1251")
    except FileNotFoundError:
        print("Нет файла:", path.name)
        return ""

text = read_text_file(Path("zametki.txt"))

Второй — ввод с клавиатуры. Человек может ввести что угодно, поэтому спрашиваем в цикле, пока ответ не подойдёт. break стоит в else: из цикла выходим только при удачном разборе.

while True:
    answer = input("Сколько потратили? ")
    try:
        amount = int(answer)
        if amount <= 0:
            raise ValueError("нужно число больше нуля")
    except ValueError as e:
        print("Не подходит:", e)
    else:
        break
print("Записал:", amount)

Пример диалога (ввод человека — после вопроса):

Сколько потратили? сто
Не подходит: invalid literal for int() with base 10: 'сто'
Сколько потратили? -50
Не подходит: нужно число больше нуля
Сколько потратили? 450
Записал: 450

Сохраните функцию read_text_file у себя и откройте ею старый текстовый файл, созданный в Блокноте Windows, и новый — созданный в VS Code. Посмотрите, в какую ветку ушёл каждый.

Проверьте себя

Задание. Что выведет программа? Введите название кодировки, как в выводе.

Исходный код для этого задания:

data = "Привет".encode("cp1251")
try:
    text = data.decode("utf-8")
    enc = "utf-8"
except UnicodeDecodeError:
    text = data.decode("cp1251")
    enc = "cp1251"
print(enc)

Подсказка: Байты получены в одной кодировке, а читаем сначала в другой. Получится ли это, и какая ветка выполнится?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Список answers имитирует то, что человек по очереди вводит в программу. Что выведет программа? Введите два числа через пробел, как в выводе.

Исходный код для этого задания:

answers = ["", "двадцать", "-5", "250", "300"]
attempts = 0
for a in answers:
    attempts += 1
    try:
        n = int(a)
        if n <= 0:
            raise ValueError("нужно больше нуля")
    except ValueError:
        continue
    else:
        break
print(attempts, n)

Подсказка: Идите по списку и после каждого ответа отмечайте: значение attempts, значение n, какая ветка сработала. Помните: n присваивается до проверки на ноль.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Грязная выгрузка расходов». Выгрузка чеков заполнялась руками, в ней попадаются пустые суммы, слова вместо чисел, отрицательные суммы, строки без разделителя, дробная часть через запятую. Напишите утилиту: читает файл rashody_gryaz.txt (utf-8, данные целиком ниже, 52 строки «статья;сумма»), разбирает каждую строку через line.split(«;») и свою функцию parse_amount(text) — она меняет запятую на точку, превращает текст в float и через raise ValueError отбраковывает сумму ≤ 0. Плохие строки программа не роняет, а пропускает и считает. В конце печатает общую сумму хороших строк, округлённую round() до целого, и число пропущенных. Проверьте и на своей выгрузке. В поле ответа — общая сумма для данных из условия. Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. в except указан конкретный тип, а не голый except;
4. отрицательная сумма отбраковывается вашим raise с понятным сообщением.

Связь;960,86

Аптека;2969

Одежда;140

Еда;1183

Связь;2443

Связь;

Связь;776

Кино;364

Связь;314

Связь;1045

Еда;1871

Кино;461

Такси;2584

Кино;1151

Аптека;1755

Кино;788,04

Связь;бесплатно

Аптека;2218

Аптека;832

Аптека;1445

Такси;0

Кино;1834

Связь;1360

Связь;2307

Аптека;420,05

Аптека

Аптека;1739

Одежда;231

Аптека;0

Аптека;

Одежда;216,85

Еда;1594,73

Связь

Аптека;2944,74

Одежда;0

Одежда;1472

Еда;1108

Еда;1977

Кино;1549

Кино;835

Связь;500

Еда;1122

Кино;2455

Еда;2721,22

Аптека;297

Связь;386

Связь;714

Аптека;0

Кино;12р

Еда;2340

Еда;751

Связь;1760

Подсказка: Строка без «;» тоже даёт ValueError при распаковке cat, s = line.split(«;») — поэтому split можно держать внутри того же try.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Делим счета». Друзья ведут журнал общих счетов: строка «кто платил;сумма;на сколько человек». Журнал ниже целиком (48 строк, файл scheta.txt, utf-8). Напишите утилиту: для каждой строки считает долю одного человека int(сумма) // int(человек) и складывает доли. Строки с нулём человек и строки с нечисловой суммой или числом людей программа не роняет, а считает отдельно — двумя разными except, у каждого свой счётчик. В конце печатает сумму долей и оба счётчика. В поле ответа — сумма долей для данных из условия. Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. ZeroDivisionError и ValueError обрабатываются в разных except;
4. программа печатает, сколько строк попало в каждую группу.

Кира;4800;4

Жанна;8500;4

Вера;15к;6

Кира;6400;7

Кира;;2

Борис;6700;6

Егор;тысяча;3

Кира;5800;0

Жанна;6100;1

Кира;5100;7

Вера;2500;

Илья;5900;0

Илья;2600;6

Лев;2300;3

Кира;7000;7

Лев;4000;3

Глеб;;4

Жанна;4900;1

Жанна;6000;2

Дина;5800;

Дина;тысяча;3

Кира;1800;6

Борис;15к;5

Анна;4600;8

Кира;7300;2

Лев;7800;4

Кира;;3

Борис;15к;3

Жанна;8400;1

Вера;1900;6

Дина;8100;7

Глеб;6800;4

Кира;800;6

Вера;3500;4

Глеб;6600;6

Борис;8500;8

Глеб;8200;0

Анна;тысяча;3

Лев;8600;2

Кира;8100;5

Кира;4700;0

Кира;4500;1

Вера;5900;8

Дина;7500;2

Илья;2800;8

Глеб;8200;2

Дина;1800;5

Илья;7900;2

Подсказка: Разбейте строку через split(«;»), деление положите в try, после него — except ZeroDivisionError и except ValueError, каждый увеличивает свой счётчик.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Что дальше

Теперь вы умеете читать текст ошибки снизу вверх, перехватывать конкретные исключения через try/except, пропускать битые строки таблицы, разделять успех и завершение через else и finally и останавливать свою функцию понятным сообщением через raise. В следующем уроке займёмся датами: научим программу считать сроки, разницу между днями и переводить дату из текста в объект и обратно — и там же пригодится try, когда дата записана с ошибкой.

← Назад · ↑ В начало урока · ⌂ К навигатору курса · Вперёд →

Школа Виктора Комлева