Print Debugging Is Still Good Debugging
Print Debugging Is Still Good Debugging
There is a point in almost every debugging session where you stop guessing the bug and start setting up the investigation.
- You open the debugger.
- Add breakpoints.
- Configure the launch settings.
- Restart the application.
- Step through several functions.
- Inspect a few objects.
And eventually discover that the value you needed to see could have been found with:
⧉
1 | |
I have done this more times than I can count.
Modern debugging tools are powerful. Proper logging is essential in many applications. Tracing, profiling, error monitoring, and interactive debuggers can reveal problems that a few printed values never will.
But for quick fixes and simple applications, print() is still one of the most useful debugging tools available.
Sometimes it is the best one.
The Tool Developers Are Supposed to Outgrow
Print debugging is often presented as something beginners do before they learn “real” debugging.
The implied progression looks like this:
Beginner → print() Experienced → debugger Professional → logging and observability platform
Real development does not work that neatly.
Experienced programmers still use print statements because they are:
- immediate
- visible
- flexible
- available everywhere
- easy to remove
- usually good enough
A debugger may be more powerful.
That does not mean it is always more useful.
The best debugging tool is the one that answers the current question with the least friction.
Sometimes that is a breakpoint.
Sometimes it is a structured log.
Sometimes it is one ugly print statement in the middle of a loop.
What Print Debugging Actually Does Well
Most small bugs begin with a simple question:
What value does this variable have here?
Or:
Is this function being called?
Or:
Which branch is the code entering?
Or:
How many times is this loop running?
Print statements answer these questions directly.
⧉
1 | |
⧉
1 | |
⧉
1 | |
⧉
1 | |
There is no setup.
There is no debugger configuration.
There is no need to pause execution or navigate an inspection panel.
You add the line, run the code, and look at the output.
For a large category of everyday bugs, that is enough.
The Speed Advantage
The main advantage of print debugging is not sophistication.
It is speed.
Imagine a Django view returning the wrong template:
⧉
1 2 3 4 5 6 7 8 | |
You could start an interactive debugger and step through the request.
Or you could add:
⧉
1 2 | |
Run the request once.
See the problem.
Remove the statements.
Done.
The debugger gives you more control, but more control was not the requirement.
The requirement was knowing which template had been selected.
When the question is simple, the debugging method should be simple too.
It Shows the Program as It Runs
Breakpoints pause a program.
That is often useful, but pausing can also change how the program behaves.
Timing-sensitive code, concurrent tasks, network operations, animations, and request flows may behave differently when stopped.
Print debugging lets the application continue running.
You can observe:
- the order of operations
- repeated function calls
- values changing over time
- branches taken during execution
- data passing through several layers
- events that happen too quickly to inspect manually
For example:
⧉
1 2 3 | |
The output gives you a simple execution trace:
⧉
1 2 3 | |
Add one more line:
⧉
1 2 3 4 | |
Now you can see input and output together.
It is not a full tracing system.
It often does not need to be.
Print Statements Can Be Placed Anywhere
Interactive debuggers work best when the runtime, editor, and execution environment cooperate.
Print statements have fewer requirements.
They work in:
- scripts
- command-line applications
- Django views
- management commands
- background tasks
- test runs
- containers
- remote development environments
- small automation tools
- temporary data-processing jobs
They also work when the bug appears in code that is awkward to pause.
You can place a print statement:
- before a return
- inside a condition
- inside a loop
- during exception handling
- between two transformations
- immediately before a database query
- immediately after an API response
⧉
1 2 3 4 5 | |
This gives you a before-and-after comparison with almost no effort.
It Forces You to Ask a Specific Question
One underrated advantage of print debugging is that it forces you to decide what you are trying to learn.
A debugger can expose an entire runtime state.
That is powerful, but it can also encourage wandering.
You pause the program and start inspecting everything:
- local variables
- request data
- object attributes
- stack frames
- imported modules
- unrelated values
A print statement is more deliberate:
⧉
1 2 | |
You have defined the question.
You have selected the evidence.
You are testing a specific assumption.
That often leads to faster debugging because most bugs are caused by one incorrect assumption:
- the function was never called
- the condition was false
- the value was already modified
- the list was empty
- the object was
None - the wrong ID was passed
- the code ran twice
Print debugging is good at confirming or rejecting those assumptions quickly.
The Labels Matter
A plain print statement can create more confusion than it solves.
This is not helpful:
⧉
1 | |
Output:
⧉
1 | |
What is 12?
A user ID?
A count?
A price?
A status code?
Label the output:
⧉
1 | |
Better still, include the location or stage:
⧉
1 | |
For several values, use a clear structure:
⧉
1 2 3 4 5 6 7 8 | |
Useful print debugging should make the output easier to understand, not simply produce more output.
Print the Boundaries
A good place to add print statements is at the boundaries between parts of a program.
For example:
- when a function receives data
- before data is transformed
- after data is transformed
- before calling another service
- after receiving its response
- before saving to the database
- after reading from the database
⧉
1 2 3 4 5 6 7 8 | |
This helps answer an important debugging question:
Did the wrong value enter this function, or did this function create the wrong value?
Without checking the boundary, developers often debug the wrong layer.
Print Debugging in Django
Print statements are especially useful while developing small Django applications.
A view can quickly confirm request data:
⧉
1 2 3 4 5 | |
A form can show validation results:
⧉
1 2 3 4 | |
A query can be inspected:
⧉
1 2 3 4 | |
A model method can expose its inputs:
⧉
1 2 3 4 5 6 7 8 9 | |
For a small local project, these statements can reveal the problem before a more elaborate debugging setup would even be ready.
The Quick-Fix Workflow
For simple problems, my usual print-debugging workflow looks like this:
- Identify the first assumption that might be wrong.
- Print the value related to that assumption.
- Run the smallest action that reproduces the bug.
- Move the print statement earlier or later in the flow.
- Repeat until the value changes unexpectedly.
- Fix the cause.
- Remove the temporary output.
Suppose a calculated total is wrong:
⧉
1 2 3 4 | |
Start with:
⧉
1 2 3 4 5 6 7 8 9 10 11 | |
If the subtotal is already wrong, inspect the items.
If the subtotal is correct and the discount is wrong, inspect get_discount().
If both are correct, inspect the final operation or the code using the result.
You are narrowing the failure point one boundary at a time.
When a Debugger Is Better
Print statements are not superior in every situation.
A proper debugger is better when you need to:
- pause execution
- inspect a large object graph
- examine several stack frames
- step through complex branching logic
- change values during execution
- inspect state without editing the code
- repeatedly investigate the same execution path
For example, stepping through a complicated recursive algorithm is usually easier with a debugger than with dozens of print statements.
The debugger gives you control over the runtime.
Print debugging gives you a lightweight trace.
They solve different problems.
When Logging Is Better
Print statements are also not a replacement for application logging.
Logging is better when output must be:
- preserved
- searchable
- filtered by severity
- collected from multiple processes
- analyzed after a failure
- sent to monitoring systems
- used in production
- associated with timestamps and request IDs
Production code should not rely on random print statements scattered across the application.
A logging call provides structure:
⧉
1 2 3 4 5 6 7 8 9 | |
Logging is part of the application.
Print debugging is usually temporary investigation.
The Problem With Tooling by Default
Debugging tools can become another form of overengineering.
A developer sees a wrong value and immediately reaches for:
- a remote debugger
- a tracing service
- a logging framework configuration
- a browser extension
- an IDE plugin
- a profiling suite
- a monitoring dashboard
All of those tools can be useful.
But they also add:
- setup time
- configuration
- unfamiliar interfaces
- additional failure points
- more context switching
Sometimes you spend more time preparing to observe the bug than fixing it.
This is the same mistake developers make elsewhere:
choosing the most capable tool instead of the simplest sufficient tool.
Make Temporary Prints Easy to Find
One problem with print debugging is forgetting to remove the statements.
A simple prefix makes them easy to search for:
⧉
1 | |
Or:
⧉
1 | |
Before committing, search the project:
⧉
1 | |
You can also inspect the diff:
⧉
1 | |
Temporary debugging code should rarely survive code review unnoticed if the changes are reviewed properly.
Print Debugging Works Well With Tests
Print statements are also useful while writing or fixing tests.
Suppose a test fails because two objects are different:
⧉
1 | |
Before changing the assertion, inspect both values:
⧉
1 2 | |
For nested data:
⧉
1 2 3 4 5 6 7 8 | |
This can quickly reveal:
- a missing key
- a type mismatch
- different ordering
- an incorrect date format
- an unexpected
None - extra whitespace
Once the problem is understood, remove the prints and keep the meaningful assertion.
Better Than print() Without Becoming Complicated
Plain print() is often enough, but Python includes a few simple tools that improve readability.
Use repr() to expose invisible characters:
⧉
1 | |
This can reveal:
⧉
1 | |
instead of:
⧉
1 | |
Use type() to confirm the data type:
⧉
1 2 | |
Use pprint() for nested structures:
⧉
1 2 3 4 | |
Use f-string debugging syntax:
⧉
1 2 | |
Output:
⧉
1 2 | |
These are still simple tools.
They keep the debugging loop fast without introducing a full debugging setup.
A Better Default
Instead of asking:
What is the most advanced tool available for debugging this?
Ask:
What is the quickest reliable way to answer the next question?
Start with the smallest useful tool.
Maybe that is:
⧉
1 | |
If it answers the question, fix the bug and move on.
If it does not, increase the level of tooling:
Print statement
↓
Targeted logging
↓
Interactive debugger
↓
Profiler or tracing
↓
Production observability
This is not a strict hierarchy.
It is a reminder to add complexity when the investigation requires it.
The Quiet Advantage of Print Debugging
Print debugging has survived because it has almost no ceremony.
It does not need to be installed.
It does not need to be configured.
It does not care which editor you use.
It does not require a specific project structure.
It asks only one thing:
What do you want to see?
Then it shows you.
There are better tools for complex investigations.
There are safer tools for production.
There are more structured tools for long-term monitoring.
But when a simple application behaves incorrectly and you need an answer now, a well-placed print statement is still hard to beat.
You do not get extra points for debugging with the most impressive tool.
You get results for finding the bug clearly and quickly.
Use the debugger when you need a debugger. Use logging when you need logging. But do not underestimate the value of printing one variable and seeing the truth immediately.