, ,

Debugging and Small Tests That Keep Code Honest (Python for Beginners, Part 18)

Debug Python with print and the built-in breakpoint debugger, write small pytest tests that assert the right answers, and dodge the floating point equality trap.

Python for Beginners · Part 18 of 20

Code that runs is not the same as code you trust. Your tracker works today, but it saves data, does maths on money, and now runs unattended, which means a quiet mistake could go unnoticed for weeks. This part is about catching mistakes on purpose: finding them fast when something breaks, and writing a few small tests that shout the moment a change breaks something that used to work.

The short versionTo debug, reproduce the problem, then look inside with print or a real debugger you open with breakpoint(). To stay safe as you change code, write a handful of tests: small functions that call your code and assert the answer, run with pytest. A few tests on the tricky parts catch far more than their size suggests.

This part draws on functions from Part 9, the automation script from Part 17, and pip from Part 14 for the test tool. You need Python 3 and about thirty minutes. Every snippet runs as printed.

Looking inside a running program

The oldest debugging tool is print, and it is not something to be ashamed of. Dropping a print in to see a value at a certain point is fast and often enough. Anyone who tells you real programmers never print debug has forgotten how much they do it. That said, Python has a proper debugger built in, and one word opens it. Put breakpoint() on a line and run the file; execution pauses there and gives you an interactive prompt inside your program:

python
def total(items):
    result = 0
    for n in items:
        breakpoint()
        result += n
    return result

print(total([10, 20]))
pdb
-> result += n
(Pdb) p n
10
(Pdb) c

At that (Pdb) prompt you inspect anything. p n prints the value of n right now, c continues to the next time the line is hit or the program ends. The debugger lets you watch values change step by step instead of guessing, which is the difference between poking at code and understanding it.

At the (Pdb) promptWhat it does
p namePrint the value of a variable
nRun the next line, stay in this function
sStep into a function being called
cContinue until the next stop or the end
qQuit the debugger

Table 1. The five debugger commands that handle almost every session.

A test is just code that checks your code

A test is a small function that runs a piece of your program and asserts what the answer should be. The word assert checks that something is true and does nothing when it is, or stops loudly when it is not. Here is a plain test with no tools at all, for the monthly_totals function from Part 17:

test_expenses.py
from monthly_summary import monthly_totals

def test_groups_by_month():
    data = [
        {'date': '2026-06-01', 'amount': 100.0},
        {'date': '2026-06-15', 'amount': 110.0},
        {'date': '2026-07-02', 'amount': 40.0},
    ]
    totals = monthly_totals(data)
    assert totals['2026-06'] == 210.0
    assert totals['2026-07'] == 40.0

Now run it. The standard tool is pytest, a package you install with pip. It finds files named test_ something, runs every function named test_ something, and reports which passed:

terminal
pip install pytest
pytest -q
output
.                                    [100%]
1 passed in 0.01s

The single dot is your passing test. Change monthly_totals in a way that breaks the grouping, run pytest again, and the dot becomes an F with a clear report of what it expected against what it got. That is the whole point: the test remembers the correct answer so you do not have to.

The bug that hides in your totals

Here is a real trap, and a reason to test money code in particular. Computers store decimals in binary, and some simple looking sums do not land where you expect:

repl
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

This is not a Python bug; every language that uses standard floating point does the same. It means a test that compares two float results with == can fail even when your logic is right. The fix is to compare with a tolerance using math.isclose, which asks whether two numbers are close enough rather than exactly equal:

test_expenses.py
import math

def test_total_is_close():
    total = 0.1 + 0.2
    assert math.isclose(total, 0.3)
output
1 passed

Knowing this saves you an afternoon of staring at a test that looks obviously correct. When money and floats meet, compare with math.isclose, or work in whole cents as integers, and never lean on exact equality for a calculated decimal.

How much testing is enough

Here is the opinion, against two loud camps. You do not need to test every line, chase a coverage percentage, or follow test driven development to the letter as a beginner. You also should not skip tests entirely and promise to add them later, because later rarely comes. The useful middle is a handful of tests on the parts that are easy to get wrong: parsing input, totalling money, edge cases like an empty file or a missing field. Those few tests catch most real regressions for a fraction of the effort, and they are the ones a reviewer actually wants to see.

A test is also a form of documentation. When someone reads test_groups_by_month, they learn exactly what monthly_totals is supposed to do from a concrete example, which is often clearer than a paragraph describing it. Write tests for the behaviour you would be upset to see break, and stop there until the code grows.

Gotcha: a test that passes by doing nothingA test with no assert in it always passes, because nothing ever checks a result. It gives you a green tick and zero safety. If a test cannot fail, it is not testing anything. Every test needs at least one assertion that would genuinely break if the code were wrong, and it is worth deliberately breaking your code once to confirm the test actually catches it.
Why this matters on the jobChanging code without tests is how working software quietly breaks. Teams rely on a test suite so anyone can make a change and know within seconds whether they broke something that mattered. New engineers who add a small test with their fix, and who can drop a breakpoint() to understand a failure instead of guessing, are trusted with bigger changes sooner. This is the habit that separates code that limps along from code a team can build on.
Real interview question"Why does 0.1 + 0.2 not equal 0.3 in Python?" A clean answer: floating point numbers are stored in binary, and many decimals cannot be represented exactly, so the sum is very slightly off. The practical takeaway they want to hear is that you compare floats with a tolerance, using math.isclose, rather than exact equality. It is a small question that reveals whether you have been bitten by real number handling.
Try it yourselfWrite a function add(a, b) that returns their sum, and a test test_add that asserts add(2, 3) equals 5. Run it with pytest. Expected output:
expected output
1 passed
Show solution
test_math.py
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5

Put both in a file whose name starts with test_, then run pytest. Because these are whole numbers, plain == is safe here; only calculated decimals need math.isclose.

Questions about debugging and tests

Is print debugging bad practice? No. It is quick and everyone uses it. Learn breakpoint() as well, because a debugger is far better when you need to watch several values change over many steps.

Do I need pytest, or is there a built in option? Python ships with unittest, which needs no install. Most people prefer pytest because tests are plain functions with plain assert. Either is fine; pytest is the common choice.

Where do test files go? Alongside your code or in a tests folder, named starting with test_ so pytest finds them automatically. The functions inside also start with test_.

What is the difference between assert in a test and in real code? In a test it checks the expected result. In production code, be careful: assertions can be switched off with an optimisation flag, so do not use them for real input validation, which is the job of the exceptions from Part 12.

How many tests should a small project have? Enough to cover the parts you would hate to see break silently. For the tracker that is totalling, parsing, and the empty or missing cases, not every trivial line.

Logging, for code that runs without you

A print statement is fine while you sit and watch a program, but the automated script from Part 17 has nobody watching it. When it misbehaves at three in the morning, you want a record of what happened. The standard logging module writes timestamped messages you can leave in the code permanently, then turn up or down without deleting anything.

python
import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger('tracker')

log.info('loaded %d expenses', 12)
log.warning('rate service slow')
output
INFO:tracker:loaded 12 expenses
WARNING:tracker:rate service slow

Unlike a bare print, every message carries a level, so you can leave detailed notes in the code and choose how much to show. Set the level to WARNING in production and the routine INFO lines go quiet, while anything that actually needs attention still comes through. You can also point logging at a file so the record survives after the run, which is exactly what an unattended job needs.

LevelUse it for
DEBUGDetailed traces while developing
INFONormal events worth recording
WARNINGSomething odd, but the program goes on
ERRORSomething failed and needs attention

Table 2. The logging levels, from everyday noise to real problems.

For a scheduled script, a couple of log.info lines at the start and finish, plus log.exception inside your except blocks from Part 12, turn a silent failure into a readable story you can follow the next morning. That small habit is what makes an automation trustworthy rather than merely working, because when it does break, and eventually it will, the log tells you exactly where.

In practiceTeams treat logs as the first place to look when something goes wrong in production. A script that logs what it did, and logs the full error when it fails, is one an on call engineer can diagnose in minutes instead of guessing in the dark.
You can nowYou can now debug with print and with the built in debugger through breakpoint(), write small tests that assert the right answers, run them with pytest, and avoid the floating point equality trap that fools people testing money. Your tracker is now something you can change without fear. In Part 19 you polish it into a finished portfolio project, with a README and a clean structure to show off.
reproduce isolate fix test it stays

Figure 1. Reproduce it, narrow it down, fix it, then add a test so it does not come back.

sample datamonthly_totals()assert == 210.0green means the answer still matches
A test feeds known data in and asserts the result equals the answer you expect.
Python for Beginners · Part 18 of 20
« Previous: Part 17  |  Complete Guide  |  Next: Part 19

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading