November 9, 2026•7 min read

Python debugging: the script that exits 0 with the wrong total

A totals script runs clean, prints a number, and the number is wrong. With no traceback to follow, you stop the program at the line that writes the value and read the dict from pdb.

Read in Indonesian

You run your script. It exits 0 and prints a total. The total is wrong.

Nothing raised and nothing printed a warning, so there is no line number to start from.

Key takeaways

  • A wrong answer with no traceback has no line to follow, so you stop the program where the value is written instead of reading an error.
  • breakpoint() starts pdb inside the loop, and p prints the value at that moment: the same day arrives twice and the second write replaces the first.
  • totals[day] = totals.get(day, 0) + session["minutes"] adds instead of overwriting, which is the whole fix.
  • A breakpoint left in a batch job waits on stdin forever, so the checker fails when breakpoint() is still in the file.

A wrong number with no traceback

The script adds up practice minutes per day. The data is three sessions, and two of them fall on the same day.

JSON
[
  {"date": "2026-11-03", "minutes": 20},
  {"date": "2026-11-03", "minutes": 45},
  {"date": "2026-11-04", "minutes": 30}
]

The script reads the file, builds a dict of minutes per day, and prints each day followed by a total.

PYTHON
# totals.py
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
sessions = json.loads(path.read_text())

totals = {}
for session in sessions:
    day = session["date"]
    totals[day] = session["minutes"]

for day in sorted(totals):
    print(f"{day}  {totals[day]}")
print(f"total {sum(totals.values())}")

This is the real run, measured on Python 3.14.9 on Linux.

TEXT
$ python3 totals.py sessions.json
2026-11-03  45
2026-11-04  30
total 75

The exit code is 0. Now check the arithmetic by hand. On 2026-11-03 you practiced 20 and 45 minutes, so that day should be 65. 2026-11-04 is 30. The total should be 95, not 75.

Nothing raised because the loop assigns where it should add. The line totals[day] = session["minutes"] is legal Python. It stores a number under a key, and if the key already exists it replaces the old value. There is no TypeError to raise and no traceback to print.

Stop it where it goes wrong

A traceback points at a line that failed. Here no line failed, so you stop the program at the line that writes the wrong value.

Put breakpoint() inside the loop, on the line before the assignment, indented like the lines around it. Then run the script as usual. The call starts pdb, and the program waits at the (Pdb) prompt.

Python 3.14 prints the working directory in front of the file name. It is cut to totals.py here so the lines fit.

TEXT
$ python3 totals.py sessions.json
> totals.py(12)<module>()
-> breakpoint()
(Pdb) p totals
{}
(Pdb) p session
{'date': '2026-11-03', 'minutes': 20}
(Pdb) n
> totals.py(13)<module>()
-> totals[day] = session["minutes"]
(Pdb) n
> totals.py(10)<module>()
-> for session in sessions:
(Pdb) p totals
{'2026-11-03': 20}
(Pdb) c
> totals.py(12)<module>()
-> breakpoint()
(Pdb) p session
{'date': '2026-11-03', 'minutes': 45}
(Pdb) p totals
{'2026-11-03': 20}
(Pdb) n
> totals.py(13)<module>()
-> totals[day] = session["minutes"]
(Pdb) n
> totals.py(10)<module>()
-> for session in sessions:
(Pdb) p totals
{'2026-11-03': 45}
(Pdb) q
Quitting pdb will kill the process. Quit anyway? [y/n] y

You do not need to type this exact session. Six commands matter, and each one answers a question:

  • p totals prints the repr of totals. It answers what the dict holds right now.
  • p session prints the item the loop is working on.
  • n runs the current line and stops at the next one.
  • c continues until the next breakpoint(), which here is the next pass of the loop.
  • l lists the source lines around where you are stopped.
  • q quits. It asks Quitting pdb will kill the process. Quit anyway? [y/n], because quitting ends the program.

The two hits prove the cause. On the first hit session is the 20 minute entry and totals is empty. On the second hit session is 2026-11-03 again, this time with 45, and totals already holds 20 for that day. After n runs the assignment, totals holds 45. The same day arrives twice, and the second write replaces the first 20 with 45.

One line that adds instead of overwrites

The fix is one line:

totals[day] = totals.get(day, 0) + session["minutes"]

totals.get(day, 0) gives the minutes stored so far for that day, or 0 when the day is new. Delete the breakpoint() line and the file looks like this.

PYTHON
# totals.py
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
sessions = json.loads(path.read_text())

totals = {}
for session in sessions:
    day = session["date"]
    totals[day] = totals.get(day, 0) + session["minutes"]

for day in sorted(totals):
    print(f"{day}  {totals[day]}")
print(f"total {sum(totals.values())}")

Both sessions on 2026-11-03 now count, and the run exits 0.

TEXT
$ python3 totals.py sessions.json
2026-11-03  65
2026-11-04  30
total 95

The checker

The arithmetic you did by hand can become a script you rerun. This checker runs totals.py on the same three sessions and compares the output with your numbers.

PYTHON
# check_totals.py
import json
import subprocess
import sys
import tempfile
from pathlib import Path

SESSIONS = [
    {"date": "2026-11-03", "minutes": 20},
    {"date": "2026-11-03", "minutes": 45},
    {"date": "2026-11-04", "minutes": 30},
]
EXPECTED = "2026-11-03  65\n2026-11-04  30\ntotal 95"


def report(results):
    for label, ok, detail in results:
        print(f"{'PASS' if ok else 'FAIL'}  {label}" + (f"  <- {detail}" if not ok and detail else ""))
    passed = sum(1 for _, ok, _ in results if ok)
    print(f"\n{passed} of {len(results)} checks passed")
    return passed == len(results)


def main():
    folder = Path(tempfile.mkdtemp())
    data = folder / "sessions.json"
    data.write_text(json.dumps(SESSIONS))
    run = subprocess.run(
        [sys.executable, "totals.py", str(data)],
        capture_output=True, text=True, input="", timeout=20,
    )
    results = [("the script exits 0", run.returncode == 0, f"exit {run.returncode}")]
    by_day = {}
    for line in run.stdout.strip().splitlines()[:-1]:
        day, minutes = line.split()
        by_day[day] = int(minutes)
    results.append(("2026-11-03 adds up both sessions", by_day.get("2026-11-03") == 65, str(by_day.get("2026-11-03"))))
    results.append(("2026-11-04 keeps its single session", by_day.get("2026-11-04") == 30, str(by_day.get("2026-11-04"))))
    results.append(("the report prints the totals it computed", run.stdout.strip() == EXPECTED, repr(run.stdout.strip())))
    source = Path("totals.py").read_text()
    results.append(("no breakpoint() left in the script", "breakpoint()" not in source, "remove it before shipping"))
    return 0 if report(results) else 1


if __name__ == "__main__":
    sys.exit(main())

Run it against the starter script and 3 of 5 checks pass. The exit code is 1.

TEXT
$ python3 check_totals.py
PASS  the script exits 0
FAIL  2026-11-03 adds up both sessions  <- 45
PASS  2026-11-04 keeps its single session
FAIL  the report prints the totals it computed  <- '2026-11-03  45\n2026-11-04  30\ntotal 75'
PASS  no breakpoint() left in the script

3 of 5 checks passed

The two FAIL lines are the same bug seen twice. Run it against the fixed script and all 5 pass. The exit code is 0.

TEXT
$ python3 check_totals.py
PASS  the script exits 0
PASS  2026-11-03 adds up both sessions
PASS  2026-11-04 keeps its single session
PASS  the report prints the totals it computed
PASS  no breakpoint() left in the script

5 of 5 checks passed

The last check fails if breakpoint() is still in totals.py. A breakpoint left in a batch job waits on stdin forever, so you want to find it before you ship.

What pdb cannot tell you

p shows the value at that moment, not its history. To learn how a value got there, you step with n and print again, as the transcript did.

A breakpoint also stops a batch job. The program does not finish until someone answers at the prompt.

PYTHONBREAKPOINT=0 disables breakpoints without editing the file. On this script it only hides the debugger.

TEXT
$ PYTHONBREAKPOINT=0 python3 totals.py sessions.json
2026-11-03  45
2026-11-04  30
total 75

The breakpoint is skipped, the script exits 0, and the bug stays.

When a number is wrong and nothing raised, stop the program at the line that writes the value, because the traceback is not coming.

Get better at shipping code.

Join the other developers getting tech updates, tips, and relaxed tutorials in their inbox every week.

Stay in the loop

Tech updates, coding tips, and relaxed insights straight to your inbox every week.

No spam. Unsubscribe anytime.

Read next

See all
Struktur DataWhat a Linked List Actually Is

A linked list is a linear chain of nodes, each holding data and a pointer to the next node. Here is what that chain looks like, what it costs next to an array, and the same minimal implementation in Java, Python, and JavaScript.

For developers who learn by struggling

© 2026 Inva.dev.