Debugging Interview Practice: Read Logs, Form Hypotheses, and Fix a Small Repository

Build debugging interview skills with a runnable Python repository, log analysis, testable hypotheses, a focused cache fix, and clear regression evidence.

Author: PracHub

Published: 9/15/2026

Debugging Interview Practice: Read Logs, Form Hypotheses, and Fix a Small Repository

September 15, 2026

Quick Overview

Practice a complete debugging investigation with a small Python repository, actual logs, competing hypotheses, a targeted cache repair, and boundary tests.

Software EngineerFree

Good debugging interview practice produces more than a patch. It produces an explanation another engineer can check: what failed, which observation narrowed the search, why the changed lines address the cause, and which tests protect the intended behavior. Practice building that chain of evidence in a small repository before adding a timer or a larger framework.

Below, you will diagnose a cache that keeps serving an old value. The exercise contains two related defects, so a fix that passes the obvious reproduction can still be incomplete. Afterward, use PracHub's Python debugging exercise to practice explaining unfamiliar code with the same discipline.

Debugging interview practice workspace with logs a notebook and a focused code change

What this practice is designed to train

Official example: Goldman Sachs describes a Software Diagnostics challenge in an existing Django, Node.js, or Spring Boot application. Its preparation advice includes understanding unfamiliar code, writing tests, and making targeted changes. That supports practicing repository diagnosis; it does not establish a universal interview format. Goldman Sachs' HackerRank guide.

Evidence boundary: the repository, logs, hypotheses, and review criteria in this article are original PracHub practice material. They are not a company's interview repository or scoring rubric. No candidate reports are used to claim particular bugs, tools, time limits, or hiring outcomes.

Our preparation recommendation: evaluate the quality of your investigation as well as the final result. A candidate who changes a comparison operator by intuition should still be able to explain the input that distinguishes the two operators. A candidate who cannot finish should leave enough evidence for someone else to continue.

For broader navigation techniques, the existing-codebase interview guide covers orienting yourself in unfamiliar projects. This article concentrates on a runnable diagnosis and repair.

Establish the contract before inspecting the implementation

The cache is configured with a positive time-to-live value in milliseconds. Its injected clock returns seconds. A cached value is valid before its expiry instant and must be refreshed at or after that instant. Different keys have independent entries. Assume one caller at a time, and measure the lifetime from the start of the lookup that loads the value.

These details matter. Without a specified expiry boundary, changing <= to < would be a preference rather than a demonstrated repair. Without stated units, a suspicious number could be an undocumented convention. In an interview, clarify such a contract once and write it down.

Start with the repository's run instructions, runtime requirements, failing test, and the implementation it calls. Follow that narrow path before exploring unrelated modules. A dependency-install failure is evidence about setup; an assertion failure after successful execution is evidence about behavior. Keep the two separate in your notes.

Build the small repository and reproduce the symptom

Create a directory containing cache.py, test_cache.py, and, optionally, the replay script below. Everything uses the Python standard library. The following implementation is intentionally broken:

import logging

log = logging.getLogger(__name__)

class Cache:
    def __init__(self, clock, ttl_ms=2000):
        self.clock = clock
        self.ttl_ms = ttl_ms
        self.entries = {}

    def get(self, key, load):
        now = self.clock()
        if key in self.entries:
            value, expires = self.entries[key]
            log.info("lookup key=%s now_s=%.3f expires_s=%.3f",
                     key, now, expires)
            if now <= expires:
                return value
        value = load(key)
        self.entries[key] = (value, now + self.ttl_ms)
        log.info("load key=%s value=%s", key, value)
        return value

Use this complete test_cache.py to define the expected behavior. Each test gets a fresh cache and source counter. Advancing a fake clock makes the tests repeatable without waiting for real time to pass.

import unittest
from cache import Cache

class CacheTests(unittest.TestCase):
    def setUp(self):
        self.now = 1000.0
        self.calls = 0
        self.cache = Cache(lambda: self.now)

    def load(self, key):
        self.calls += 1
        return self.calls

    def test_first_load(self):
        self.assertEqual(self.cache.get("A", self.load), 1)

    def test_before_expiry(self):
        self.cache.get("A", self.load)
        self.now = 1001.0
        self.assertEqual(self.cache.get("A", self.load), 1)
        self.assertEqual(self.calls, 1)

    def test_after_expiry(self):
        self.cache.get("A", self.load)
        self.now = 1002.001
        self.assertEqual(self.cache.get("A", self.load), 2)

    def test_at_expiry(self):
        self.cache.get("A", self.load)
        self.now = 1002.0
        self.assertEqual(self.cache.get("A", self.load), 2)

    def test_independent_keys(self):
        self.assertEqual(self.cache.get("A", self.load), 1)
        self.assertEqual(self.cache.get("B", self.load), 2)
        self.assertEqual(self.cache.get("A", self.load), 1)

Run python3 -m unittest -v from that directory. In our local Python 3.9.6 run, three tests passed and the two expiry tests failed. Python's official unittest documentation explains discovery and selecting individual tests.

The source function returns its call count. Therefore, receiving one after expiry means the second value was not obtained. This is deliberately simpler than a network request: you can isolate cache behavior without guessing whether a remote service changed its response.

Read logs as observations, not explanations

For a visible reproduction, save this as replay.py and run python3 replay.py:

import logging
from cache import Cache
logging.basicConfig(level=logging.INFO, format="%(message)s")
now = [1000.0]
values = iter([1, 2])
cache = Cache(lambda: now[0])
print("first", cache.get("A", lambda key: next(values)), flush=True)
now[0] = 1002.001
print("later", cache.get("A", lambda key: next(values)), flush=True)

The combined local output is:

load key=A value=1
first 1
lookup key=A now_s=1002.001 expires_s=3000.000
later 1

The lookup time has advanced a little more than two seconds. The stored expiry is 3000, and the old value returns. Those are observations. “The upstream service is stale” is a hypothesis, and this trace does not support it: the second lookup never reaches the loader in this execution.

Notice that the label expires_s does not prove the value was computed in seconds. A log statement can faithfully print a wrongly calculated number. Read the calculation that produced the field before trusting its label as a specification.

Tool fact: Python's logging system supports contextual records and configurable levels and handlers. Use those controls to expose a relevant state transition, rather than scattering permanent prints through unrelated code. Python logging documentation. For this local fixture, the key and counter are synthetic; a real application may require redaction before recording user data.

Form hypotheses that make different predictions

A useful hypothesis names a cause and an observation that could disprove it. “Something is wrong with caching” is too broad. “The expiration deadline combines milliseconds with seconds” predicts a particular numeric deadline and a particular repair.

HypothesisDiscriminating check
Source is staleDid the loader run again?
Units differWhat deadline was stored?
Boundary differsWhat happens exactly at expiry?
State is sharedDoes a new cache reproduce it?

In this fixture, a first load at 1000 seconds with a 2000-millisecond lifetime should expire at 1002 seconds. The observed 3000 is exactly what adding 2000 directly would produce. That arithmetic connects the symptom to a specific expression.

A breakpoint at the cache-hit condition would let you inspect now, expires, and the returned value without changing application state. Python's pdb documentation distinguishes stepping into a call from advancing over it. Choose the operation that answers your current question; stepping through every library function is rarely useful.

Keep the next experiment small. Test the units hypothesis before rewriting the cache, replacing the clock, or changing the source loader. If the prediction fails, preserve that result and reconsider the hypothesis. An unsuccessful experiment can still eliminate a plausible explanation.

Make the targeted repair, then test the boundary

The first repair converts the configured lifetime into seconds:

-        self.entries[key] = (value, now + self.ttl_ms)
+        self.entries[key] = (value, now + self.ttl_ms / 1000.0)

The replay now refreshes correctly after expiry. Its stored deadline becomes 1002.000, and the later value becomes two. However, the complete suite still has one failure: a request at exactly 1002.0 returns the old value.

That remaining failure is not noise. The contract says validity ends at the expiry instant. Repair the comparison:

-            if now <= expires:
+            if now < expires:

All five tests now pass locally. The progression matters: two failures in the original version, one after converting units, zero after fixing the boundary. A single happy-path rerun would have accepted the incomplete repair.

Two independent proofs for a cache fix: convert milliseconds to seconds and refresh at the expiry boundary

Resist broad changes that obscure this evidence. Removing caching entirely would make the expired-value tests pass but break the before-expiry requirement. Changing the expected result to one would hide the defect. Adding a long sleep would make the test slower without clarifying the boundary.

Explain what the tests prove and what they do not

The fixture verifies first load, reuse before expiry, refresh after expiry, refresh at the boundary, and independent keys. It does not establish thread safety, bounded memory, distributed consistency, or behavior across process restarts. The cache has no eviction policy, and the test clock is deliberately controlled.

For elapsed time inside one process, Python documents time.monotonic() as unaffected by system-clock updates and returning fractional seconds. Its reference point is undefined, so elapsed differences are meaningful rather than calendar timestamps. Python clock documentation. That makes it a sensible production-clock candidate for this contract, but replacing the clock alone would not repair the unit mismatch.

If the interviewer expands the requirements, identify the additional invariant before adding tests. Concurrent misses might require a policy for duplicate loads. Slow loading raises whether lifetime starts before or after fetching. Persisted deadlines require a clock representation meaningful after restart. These are follow-up design questions, not defects the current single-caller fixture claims to solve.

Review the final diff as well as the test report. Confirm that only the intended behavior changed, diagnostic scaffolding is appropriate, and no test was accidentally skipped. A green summary is useful only when the relevant assertions actually ran.

Use repository tools when they answer a concrete question

Search for the failing function or log field with a focused code search, then follow its callers. If a regression has a known good revision and a reliable failing test, git bisect can narrow the introducing change. It is less useful when the only evidence is a vague symptom or an unstable environment.

Official Git behavior: automated bisection interprets exit zero as good, most nonzero codes in its documented range as bad, and 125 as an untestable revision to skip. An installation failure should not silently become proof of an application regression. Git bisect documentation. This exercise does not need bisection because its entire implementation is small and no commit history is supplied.

During an interview, narrate decisions rather than every keystroke. For example: “The second call never reaches the loader, so I am inspecting the cache-hit condition. The stored deadline differs by a factor of one thousand from the contract. I will fix the units, then check the exact boundary separately.”

If time expires, report the smallest reproduction, current hypothesis, last experiment, remaining failure, and next command. Do not describe an unexecuted idea as a verified fix. That handoff shows what another engineer can trust immediately.

Continue debugging interview practice with a reviewable result

These cross-company PracHub questions train related skills. They are practice material, not predictions of the exact repository or defects in your interview.

Our self-review recommendation has four parts: a reproducible failure, a hypothesis with a distinguishing observation, a patch tied to the contract, and regression evidence. Do not assign yourself an invented employer passing score. Instead, record which part remains weak and choose the next exercise accordingly.

For your next repetition, open Debug Validation Error Aggregation, write down the expected error behavior, and identify one input that could disprove your first explanation. Finish with a short change summary that names the trigger, cause, repair, and tests. That is a useful debugging deliverable even when the eventual code change is only two lines.

Sources and Further Reading

Research checked September 15, 2026. Exercises and logs are original; local tests ran on Python 3.9.6. Employer instructions govern the tools and format of an actual interview.


Comments (0)