// python interview / errors-testing

Вопросы на собеседовании: Ошибки, тесты и отладка

Исключения и их цепочки, pytest и моки, логирование и отладка — как ломаться громко и чиниться быстро. Здесь — топ-15 по частоте на реальных собесах: у первых вопросов открыт полный разбор, у остальных — устный эталон. Весь банк темы (26 вопросов) с разборами — в приложении.

Открыть тему в приложении каждый день бесплатно: 3 эталона и 3 проверки арбитром

junior: база, с которой начинают

Глянь на этот код — что он выведет и почему?

def process():
    try:
        print("try")
        return "from try"
    except ValueError:
        print("except")
    else:
        print("else")
    finally:
        print("finally")

result = process()
print(result)

как ответить Выведет: try, finally, from try. Try выполняется без исключения и доходит до return — значение к возврату уже определено, но перед фактическим выходом из функции гарантированно выполняется finally, поэтому он печатается раньше. Else не выполняется, потому что try был покинут через return, а не завершился обычным падением до конца блока — else срабатывает только при нормальном завершении try без исключения и без return/break/continue внутри.

разбор

Порядок такой: try выполняется первым; если исключения не было и try дошёл до конца блока обычным путём — выполняется else; except срабатывает только при подходящем исключении; finally выполняется всегда — было исключение или нет, был return/break/continue или нет. Ключевая ловушка именно в else: многие ошибочно думают, что он выполнится после любого «успешного» try, включая return, — но return это досрочный выход, а не обычное завершение блока, поэтому else пропускается.

Вторая ловушка, которую обычно спрашивают следом: что если finally сам содержит return или raise. Тогда finally забивает исходный return/exception из try — исходное значение или исключение теряется молча, это частый источник багов при работе с ресурсами (закрытие файла/соединения в finally с return внутри).

Практический вывод: finally — место для гарантированной очистки (закрыть файл, отпустить лок), else — место для кода, который должен выполниться только при отсутствии исключения, но не смешивать с успешным return в try — тогда else не нужен.

python-errors-testing-001 · junior · high

Чем поймать конкретное исключение — например ValueError — лучше, чем голый except: или except Exception? Когда широкий except вообще оправдан?

как ответить Голый except: ловит вообще всё, включая наследников BaseException — KeyboardInterrupt и SystemExit, то есть глушит Ctrl+C и штатное завершение процесса. except Exception чуть безопаснее, но всё равно прячет баги: опечатка в имени, TypeError от неправильного вызова — и программа едет дальше в неопределённом состоянии. Конкретный except документирует ожидаемые сбои и даёт остальному всплыть с трейсбеком. Широкий catch оправдан только на границе — верхний уровень приложения, воркер очереди — и обязательно с логированием и явным решением, что делать дальше.

разбор

Широкий except превращает баги в тихие искажения поведения: например except: вокруг блока с опечаткой в имени переменной ловит NameError и код просто «работает не так», вместо явного краша с трейсбеком, который сразу указывает на строку. Это прямо противоречит принципу fail-fast — падать громко и сразу лучше, чем прятать ошибку под catch-all.

Вторая причина — семантика самого перехвата: except Exception не ловит BaseException-наследников (SystemExit, KeyboardInterrupt, GeneratorExit), так что explicit except Exception — уже более осознанный выбор, чем bare except:. Но даже он должен быть по возможности сужен до конкретных типов, которые вызывающий код реально готов обработать.

Типичный follow-up — про границы приложения: HTTP-хендлер верхнего уровня, воркер задач, main() — там ловить широко нормально, но обязательно логировать с трейсбеком (logger.exception) и решать: вернуть 500, положить задачу в dead-letter, или re-raise. Молчаливый catch-all без логирования — это всегда антипаттерн.

python-errors-testing-002 · junior · high

Что такое EAFP и LBYL? Чем они отличаются и какой подход обычно предпочтительнее в Python и почему?

как ответить EAFP («easier to ask forgiveness than permission») — сначала выполняем операцию, а ошибки ловим через try/except; LBYL («look before you leap») — сначала проверяем условия (isinstance, hasattr, in), потом действуем. В Python идиоматичен EAFP: try/except дёшев, если исключение не срабатывает, а LBYL часто создаёт гонку между проверкой и использованием (TOCTOU) и дублирует внутреннюю логику операции.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-003 · junior · high

Расскажи, как устроен тест в pytest — как называть файлы и функции, чтобы pytest их подобрал, и почему тут не нужен класс с self, как в unittest?

как ответить pytest по умолчанию собирает файлы test_*.py или *_test.py, а внутри них функции test_*() либо методы test_* в классах Test* без __init__. Проверки — обычный python assert, а не self.assertEqual из unittest. Discovery донастраивается через testpaths/python_files в pytest.ini или pyproject.toml, но из коробки уже работает без конфига.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-004 · junior · high

Что такое fixture в pytest, и какие у неё бывают scope — чем отличаются function, class, module и session, и когда какой выбирать?

как ответить Fixture — функция с декоратором @pytest.fixture, которая готовит данные или ресурс и передаётся тесту через аргумент с тем же именем; pytest сам резолвит граф зависимостей и запускает setup/teardown вокруг yield. Scope задаёт, как часто фикстура пересоздаётся: function по умолчанию — на каждый тест, class/module/session — реже. Чем шире scope, тем меньше накладных расходов, но выше риск, что один тест испортит состояние для следующего.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-005 · junior · high

Что делает unittest.mock.patch и как им пользоваться — декоратор, контекстный менеджер, вручную start/stop?

как ответить unittest.mock.patch временно подменяет атрибут (функцию, метод, класс) на Mock/MagicMock на время теста и гарантированно восстанавливает оригинал после — даже если тест упал. Используется как декоратор (@patch('module.name')), контекстный менеджер (with patch(...) as m) или вручную через patcher.start()/patcher.stop() с явным cleanup. Аргумент — строка пути к объекту, который патчим; сам объект-заглушка передаётся как дополнительный параметр теста при использовании декоратора.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-006 · junior · high

Чем логирование лучше print для отладки и как у тебя обычно настроены уровни логов?

как ответить print — это просто вывод в stdout без уровней, фильтрации и настройки; logging даёт уровни (DEBUG/INFO/WARNING/ERROR/CRITICAL), которые можно фильтровать без правки кода, и хендлеры, разводящие вывод по файлу, stderr, Sentry и т.д. В проде уровень обычно INFO или WARNING, DEBUG включают точечно при расследовании инцидента.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-007 · junior · high

Что выведет этот код и почему?

def check(x):
    assert (x > 0, "x must be positive")
    return x

print(check(-5))

как ответить Выведет -5, ассерт не сработает никогда. assert (x > 0, "msg") — это assert <tuple>, а непустой tuple всегда truthy независимо от того, что в первом элементе. Правильный синтаксис — assert x > 0, "x must be positive" без скобок вокруг всего выражения.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-008 · junior · high

Как устроена иерархия встроенных исключений в Python и почему это важно на практике — например при выборе порядка except-блоков?

как ответить На вершине BaseException, от неё напрямую наследуются системные вещи — SystemExit, KeyboardInterrupt, GeneratorExit — и отдельно Exception, от которой уже идут все обычные ошибки: ValueError, TypeError, LookupError с наследниками KeyError и IndexError, OSError и так далее. Практический смысл в двух вещах: except-блоки нужно располагать от более специфичных к более общим, иначе общий блок перехватит исключение раньше специфичного и тот код станет недостижимым; и except Exception, в отличие от голого except, не трогает системные BaseException-наследники вроде KeyboardInterrupt.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-009 · junior · medium

Что делает raise без аргументов внутри except-блока? Когда его используешь и чем это отличается от raise НовоеИсключение(...) или raise ... from ...?

как ответить Голый raise внутри except повторно выбрасывает текущее исключение с оригинальным трейсбеком — используется, чтобы залогировать или откатить транзакцию, не глуша ошибку. raise NewError(...) внутри except создаёт новое исключение, и Python автоматически прикрепляет исходное как __context__ («During handling of the above exception…»). raise NewError(...) from original делает связь явной через __cause__ — осознанная трансляция одной ошибки в другую; from None подавляет цепочку целиком, когда исходная ошибка наружу не нужна.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-010 · junior · medium

Как в pytest параметризовать тест, чтобы прогнать одну и ту же логику на разных входных данных?

как ответить @pytest.mark.parametrize('arg,expected', [...]) генерирует отдельный тест-кейс на каждый набор параметров, каждый со своим id в отчёте: для простых значений id собирается из них самих (test_name[1-2]), а для нечитаемых объектов — из имени аргумента и индекса (test_name[arg0-expected0]). Несколько parametrize над одним тестом дают декартово произведение случаев, а pytest.param(..., id=..., marks=pytest.mark.xfail) позволяет задать читаемый id или точечно пометить конкретный кейс как xfail или skip.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-011 · junior · medium

middle: где отделяют уверенных

Расскажи про raise ... from — зачем он нужен, и чем отличаются __cause__ и __context__ у исключения?

как ответить raise NewErr() from original явно связывает исключения: NewErr.__cause__ становится original, и в трейсбеке печатается «The above exception was the direct cause of the following exception». Если просто поднять новое исключение внутри except-блока без from, Python всё равно неявно запомнит предыдущее — но в __context__, с пометкой «During handling of the above exception, another exception occurred». raise ... from None явно подавляет показ контекста, устанавливая __suppress_context__ = True.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-013 · middle · high

Чем ассерты pytest отличаются от assertEqual/assertTrue из unittest, и как pytest умудряется показывать подробный diff на голом assert?

как ответить pytest переписывает AST тестовых модулей на этапе импорта — assertion rewriting — подставляя в plain assert разбор операндов и их значений. Поэтому assert a == b показывает repr обеих сторон и diff для списков, словарей и строк без специальных assertXxx-методов, как в unittest. Работает только для файлов, которые собирает и импортирует сам pytest, отключается флагом --assert=plain или no:rewrite в конфиге плагина.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-014 · middle · high

Есть известное правило — «патчить там, где используется, а не там, где определено». Объясни, почему так, и скажи, что выведет этот тест.

# module_a.py
def get_time():
    return "real-time"

# module_b.py
from module_a import get_time

def report():
    return get_time()

# test.py
from unittest.mock import patch
import module_b

with patch("module_a.get_time", return_value="mocked"):
    print(module_b.report())

как ответить Python-модуль — это пространство имён, и from module_a import get_time в module_b создаёт в module_b собственную ссылку на объект в момент импорта. Патч module_a.get_time подменяет атрибут в module_a, но module_b.get_time — уже другая, независимая ссылка на старый объект, поэтому её значение не меняется. Правильный патч в этом случае — patch("module_b.get_time"): там, где имя реально ищется при вызове, а не там, где оно изначально объявлено.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-015 · middle · high

Когда вообще стоит мокать зависимость в тесте, а когда лучше этого не делать? Какие границы ты мокаешь в первую очередь?

как ответить Мокать имеет смысл на внешних границах системы — сеть, БД, файловая система, время, random, сторонние API: там, где реальный вызов медленный, недетерминированный или платный. Свой код мокать стоит избегать: если на собственные функции ставится больше двух моков — это сигнал менять дизайн и выносить границу явным интерфейсом. Юнит-тест с моками проверяет, что код правильно вызвал зависимость, а не что фича работает — для второго нужны интеграционные тесты поверх реальных границ.

полный разбор и проверка ответа арбитром — в приложении python-errors-testing-016 · middle · high

Соседние темы того же собеса