Debugging with print
when it is enough and how to do it properly
What it is
Dropping in a print and looking at what is in a variable is the simplest
way to debug. It is conventionally treated as unserious, and for most tasks
it is faster than a debugger and requires no setup at all.
This article is about printing in a way that actually helps, and about when you do need a debugger after all.
What it looks like in code
Bad
print(data)
print(x)
print("here")You run it, get three cryptic lines in a row and can't tell which came from where.
Good
print(f"[parse] items={len(items)} first={items[0] if items else None!r}")
print(f"[to_numbers] temp={it['temp']!r} type={type(it['temp']).__name__}")Three habits that make printing useful:
A label. [parse] tells you at once where the line came from. With a
dozen prints it saves all of the time.
!r instead of plain output. repr shows the quotes and the invisible
characters. The difference is decisive:
value = " 42 "
print(f"value={value}") # value= 42
print(f"value={value!r}") # value=' 42 'In the second case you can see immediately that the string has spaces in
it — and those spaces are exactly why int() was failing.
The type, not only the value. '4' and 4 look almost the same and
behave differently. type(x).__name__ settles the question.
Printing a structure
For dictionaries and lists this is more convenient:
import json
print(json.dumps(data, ensure_ascii=False, indent=2)[:500])ensure_ascii=False keeps non-English text readable instead of showing
escape codes. The [:500] slice keeps the terminal from flooding when there
is a lot of data.
When you need a debugger
Printing is enough while you know what to print. A debugger is for when you don't:
| Debugger | |
|---|---|
| you know which variables to look at | you don't know what is going on at all |
| one or two passes | you need to go step by step |
| the code runs quickly | every run is slow |
| everything is in your own code | the error is somewhere in a library |
The cheapest debugger in Python is one line in the right place:
breakpoint()The program stops and you land in an interactive shell where any variable
can be inspected. c continues, n is the next line, q quits.
What to tell the AI
I don't understand why this ends up as an empty list.
Where should I put two prints to make that visible?
Don't change the code, just name the places and what to print.Add debug prints to all three functions: a label, the key
values through !r, and their types. I'll remove them later.And the mandatory final step:
Remove all the debug prints we added.
Don't touch the log.* logging.What to look for
Prints left in the code. The classic: debugged it, forgot to remove
them, it shipped to the server and now clutters the output. Before
committing, search the diff for print(.
Print instead of logging in long-lived code. In a bot or on a server
print disappears along with the terminal. There you need logging
(article 4.2).
Printing in the wrong place. If the output doesn't change for clearly different inputs, the print is probably in the wrong branch — or execution never reaches it.
Secrets in a print. print(config) together with the token, and now it
is in the terminal history, or in a screenshot sent to a chat.
Cheat sheet
A label at the start of the line:
[parse] ....Always
!r— it shows spaces, quotes andNone.Print the type, not only the value.
Dictionaries — via
json.dumps(..., ensure_ascii=False)and a slice.Don't know what to look at —
breakpoint().Before committing, check the diff for leftover
print(.