Урок курса

Обработка ошибок ввода и отсутствия файла

Python для новичков

В прошлом уроке мы разобрали базовую механику try-except: как обернуть опасный код, как указать конкретный тип исключения и почему голый except — плохая идея. Сейчас этот инструмент нужно применить к двум реальным сценариям — некорректный ввод пользователя и попытка открыть файл, которого не существует.

Контекст урока: try-except и базовые исключения

Прежде чем переходить к конкретным паттернам, важно понять, с какими исключениями будем работать в этом уроке.

Первое — ValueError. Оно возникает, когда вы пытаетесь преобразовать строку в число, а строка не содержит допустимого значения. Например, int('abc') или int('') — оба вызова завершатся ValueError. Это исключение вы уже видели раньше.

Второе — FileNotFoundError. Оно появляется в тот момент, когда Python выполняет open() в режиме чтения ('r'), а файл с указанным именем не существует на диске. Технически FileNotFoundError является подклассом OSError — общего класса для ошибок операционной системы. Это значит, что его можно перехватить и через except OSError:, но точнее и понятнее писать именно except FileNotFoundError:.

Почему open() бросает исключение? Потому что Python пытается обратиться к файловой системе, получает отказ от ОС и сигнализирует об этом через исключение. Никакой «тихой» обработки по умолчанию нет — если не перехватить FileNotFoundError, программа упадёт с трейсбэком.

Важно понимать: FileNotFoundError актуален прежде всего при открытии на чтение ('r'), когда файл обязан уже существовать. Если вы открываете файл на запись ('w') и директория существует, Python создаст файл сам — FileNotFoundError не возникнет. Поэтому в этом уроке фокус на режиме чтения.

Эти два исключения покрывают разные точки программы: ValueError — при обработке пользовательского ввода, FileNotFoundError — при работе с файловой системой. В обоих случаях инструмент одинаков: try-except с явным указанием типа.

Перехват FileNotFoundError при чтении файла

Теперь посмотрим, как это выглядит на практике. Задача простая: открыть файл на чтение, но сделать это так, чтобы программа не падала, если файла нет.

Конструкция with open(...) as f уже знакома — она сама закрывает файл по выходу из блока. Здесь она остаётся без изменений, только оборачивается в try:

try:
    with open('data.txt', 'r', encoding='utf-8') as f:
        content = f.read()
    print('Файл прочитан, символов:', len(content))
except FileNotFoundError:
    print('Ошибка: файл data.txt не найден')

Что здесь происходит: open('data.txt', 'r', ...) — это та самая «опасная» строка. Если data.txt отсутствует на диске, Python не доберётся до f.read() — исключение возникнет уже при вызове open(). Блок except FileNotFoundError: ловит именно этот момент и выводит понятное сообщение вместо трейсбэка.

Обратите внимание: print с длиной контента стоит внутри try, после with-блока. Это правильно — он выполнится только если файл открылся успешно. Если бы он стоял после всего try-except, он выполнялся бы даже при ошибке, что было бы логически некорректно.

Ещё один момент: except FileNotFoundError: не перехватывает другие ошибки файловой системы — например, PermissionError (когда файл есть, но нет прав на чтение). Это нормально: мы перехватываем ровно ту ситуацию, которую ожидаем и умеем обработать. Всё остальное пусть всплывает как обычно.

Паттерн цикла повторного запроса при некорректном вводе

Одиночный try-except хорош, когда ошибка некритична и программа может продолжить работу без повторной попытки. Но с пользовательским вводом ситуация другая: если человек ввёл не число, нужно не просто вывести сообщение и двигаться дальше — нужно спросить снова. Именно для этого существует паттерн «бесконечный цикл с выходом по успеху».

Идея простая: запускаем while True — цикл, который в норме никогда не остановится сам. Внутри помещаем try-except. Если преобразование прошло успешно, вызываем break — и только тогда. Если возникло исключение, except выводит сообщение, break не вызывается, и цикл идёт на следующую итерацию: пользователь снова видит приглашение ввода.

while True:
    try:
        n = int(input('Введите целое число: '))
        break
    except ValueError:
        print('Это не число. Попробуйте ещё раз.')

print('Вы ввели:', n)

Проследим путь выполнения на двух сценариях.

Сценарий 1 — пользователь вводит 'abc':

  • int('abc') бросает ValueError
  • управление переходит в except, печатается сообщение
  • break не достигается
  • цикл начинается заново

Сценарий 2 — пользователь вводит '42':

  • int('42') возвращает 42, переменная n получает значение
  • следующая строка — break — выполняется
  • цикл завершается
  • print после цикла выводит значение n

Ключевой момент этого паттерна — break является условием выхода, а не просто завершением. Цикл работает не «пока что-то верно», а «пока не получен корректный ввод». Это архитектурное решение: вместо условия в заголовке while логика остановки вынесена внутрь try.

Такой подход хорошо масштабируется. Можно добавить ограничение диапазона значений прямо в блок try, и оно тоже будет под защитой цикла:

while True:
    try:
        age = int(input('Введите ваш возраст (1–120): '))
        if age < 1 or age > 120:
            print('Возраст вне допустимого диапазона.')
            continue
        break
    except ValueError:
        print('Введите целое число.')

print('Возраст принят:', age)

Здесь continue возвращает управление в начало цикла, если число корректное по типу, но не по диапазону. break вызывается только когда оба условия выполнены. Это не усложнение ради усложнения — это демонстрация того, что паттерн гибкий: к нему можно добавлять проверки без изменения общей структуры.

Правильное размещение break и пошаговая реализация цикла

Самая частая ошибка при написании цикла повторного запроса — break стоит не там, где нужно. Выглядит это так:

# Неправильно
while True:
    try:
        n = int(input('Введите число: '))
    except ValueError:
        print('Это не число. Попробуйте ещё раз.')
    break  # <-- здесь

break здесь стоит после блока try-except, то есть на одном уровне отступа с try. Что происходит при выполнении? Независимо от того, было исключение или нет, после обработки try-except Python доходит до break и выходит из цикла. Пользователь получает ровно один шанс — и если он ввёл 'abc', программа всё равно завершает цикл, переменная n не определена, и дальнейший код упадёт с NameError.

Правильный вариант — break внутри try, после успешного преобразования:

# Правильно
while True:
    try:
        n = int(input('Введите число: '))
        break  # выполняется только если int() не бросил исключение
    except ValueError:
        print('Это не число. Попробуйте ещё раз.')

print('Принято:', n)

Когда int(input(...)) бросает ValueError, выполнение немедленно уходит в except — строка с break просто не достигается. Цикл продолжается. Когда преобразование проходит успешно, break достигается и цикл завершается. Именно так позиция одной строки определяет, работает паттерн или нет.

Чтобы каждый раз не думать «куда поставить break», удобно держать в голове чёткую последовательность шагов.

Шаг 1.

Написать while True: — это каркас, который держит цикл активным.

Шаг 2.

Внутри сразу написать try: — весь опасный код пойдёт сюда.

Шаг 3.

В блоке try разместить вызов int(input(...)) или другое преобразование, которое может бросить исключение.

Шаг 4.

Следующей строкой после преобразования — break. Физически: сразу под строкой с int(input(...)), на том же уровне отступа внутри try.

Шаг 5.

Написать except ValueError: и внутри — сообщение пользователю. break здесь не ставить.

В итоге структура всегда выглядит так:

while True:
    try:
        # шаг 3: опасная операция
        n = int(input('Введите целое число: '))
        # шаг 4: break сразу после успеха
        break
    except ValueError:
        # шаг 5: только сообщение, без break
        print('Введите целое число, не текст.')

Если нужна дополнительная проверка значения, она добавляется между преобразованием и break — как показано в предыдущей секции. Структура при этом не меняется: break по-прежнему остаётся последней точкой в try, которой программа достигает только при полном успехе.

Запомнить порядок просто: try → операция → breakexcept → сообщение. Всё остальное — вариации внутри этого каркаса.

Попробуйте решить

Что выведет программа, если файл report.txt не существует?

try:
    with open('report.txt', 'r', encoding='utf-8') as f:
        print(f.read())
except FileNotFoundError:
    print('Файл не найден')

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку