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()startspdbinside the loop, andpprints 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.
[
{"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.
# 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.
$ 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.
$ 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 totalsprints thereproftotals. It answers what thedictholds right now.p sessionprints the item the loop is working on.nruns the current line and stops at the next one.ccontinues until the nextbreakpoint(), which here is the next pass of the loop.llists the source lines around where you are stopped.qquits. It asksQuitting 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.
# 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.
$ 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.
# 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.
$ 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.
$ 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.
$ 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.