Don’t look down on print debugging
Print debugging is a straightforward and effective method for identifying code issues, involving placing print statements, analyzing output, and removing them after resolution, while automated tests enhance the debugging process.
Read original articleThe article discusses the often-overlooked value of print debugging in programming. Despite the prevalence of sophisticated debugging tools, print debugging remains a straightforward and effective method for identifying issues in code. The author argues that print statements help programmers focus on understanding their code and verifying assumptions, making it a valuable practice rather than a shameful one. The workflow for print debugging involves discovering a problem, strategically placing print statements, analyzing the output, fixing the issue, and then removing the print statements before finalizing the code. The author emphasizes that while more advanced tools can be beneficial, print debugging should not be dismissed due to its simplicity and universality. Additionally, the article highlights the importance of building automated tests alongside code to facilitate easier debugging, as it allows developers to isolate problems more effectively. The author concludes by acknowledging that print debugging is a legitimate and powerful tool that can complement more sophisticated methods.
- Print debugging is a simple yet effective method for identifying code issues.
- The workflow involves placing print statements, analyzing output, and removing them after resolving the issue.
- Automated tests can enhance the debugging process by isolating problems.
- Print debugging should not be dismissed in favor of more complex tools.
- Understanding code through print statements can lead to better debugging practices.
Related
Profiling with Ctrl-C
Ctrl-C profiling is an effective method for identifying performance issues in programs, especially in challenging environments, despite its limitations in sampling frequency and multi-threaded contexts.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Local Variables as Accidental Breadcrumbs for Faster Debugging
The blog post highlights the significance of local variables in debugging, emphasizing their role in providing context for errors, and suggests improvements for error tracking tools like Bugsink.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Caveman Debugging in the Modern Age
Caveman debugging uses print statements to track code execution. Integrating it with IntelliJ IDEA's Live Template feature enhances productivity through custom templates, streamlining repetitive tasks and improving coding efficiency.
This isn't just an assumption I'm making: years of being in developer leadership roles, and then watching a couple of my own sons learning the practice, has shown me in hundreds of cases that if print-type debugging is seen, a session demonstrating how to use the debugger to its fullest will be a very rewarding effort. Even experienced developers from great CS programs sometimes are shocked to see what a debugger can do.
Walk the call stack! See the parameters and values, add watches, set conditional breakpoints to catch that infrequent situation? What! It remains eye opening again and again for people.
Not far behind is finding a peer trying to eyeball complexity to optimize, to show them the magic of profilers...
To see the nature of the race condition, just put some print statements in some strategic locations and then see the interleaving, out of order, duplicate invocations etc that are causing the trouble. It's hard to see this type of stuff with a debugger.
Then, when I code myself, I use print debugging like 99.9% of the time :D I have the feeling that, for me, the debugger tends to be not worth the effort. If the bug is very simple, print debugging will do the job fast so the debugger would make me waste time. If the bug is very complex, it can be difficult to know where to set the breakpoints, etc. in the debugger (let alone if there's concurrency involved). There is a middle ground where it can be worth it but for me, it's infrequent enough that it doesn't seem worth the effort to spend time making the decision on whether to use the debugger or not. So I just don't use it except once in a blue moon.
I'm aware this can be very personal, though, hence my tries to have my students get some practice with the debugger.
* Not all languages have good debuggers.
* It's not always possible to connect a debugger in the environment where the code runs.
* Builds don't always include debug symbols, and this can be very high-friction to change.
* Compilers sometimes optimize out the variable I'm interested in, making it impossible to see in a debugger. (Haskell is particularly bad about this)
* As another commenter mentioned, the delay introduced by a debugger can change the behavior in a way that prevents the bug. (E.g. a connection times out)
* In interpreted languages, debuggers can make the code painfully slow to run (think multiple minutes before the first breakpoint is hit).
One technique that is easier to do in printf debugging is comparing two implementations. If you have (or create) one known-good implementation and have a buggy implementation, you can change the code to run both implementations and print when there's a difference in the result (possibly with some logic to determine if results are equivalent, e.g. if the resulting lists are the same up to ordering).
However, if I know I'm going to be working on a project for a long time, I usually try to pay the upfront cost of setting up a debugger for common scenarios (ideally I try to make it as easy as hitting a button). When I run into debugging scenarios later, the cost/benefit analysis looks a lot better - set a breakpoint, hit the "debug" button, and boom, I can see all values in scope and step through code.
Don't care about the tool care about the performance.
Anecdotally, debuggers are faster than print statements in most cases for me. I've been able to find bugs significantly faster using a debugger than with using print statements. I still do use print statements on occasion when I'm developing something where a debugger is very complicated to set up, or in cases where I'm dealing with things happening in parallel/async, where a debugger is less suited. I'm not going to shame you for using print statements, but I do hope that you've tried both and are familiar/comfortable with both approaches and can recognise their strengths/weaknesses -- something I'm not convinced of by this author, which only outlines the strengths of one approach.
Also not a fan of the manufactured outrage of saying people are being "shamed" for using print statements. Coupled with listing a bunch of hyperbolic articles -- many of which don't even seem to be about debugging but about logging libraries.
(Also as a side note: don't forget if you are using print statements for debugging to check if your language buffers the print output!! You'll likely want to have it be unbuffered if you're using print for debugging)
I pretty much only use print debugging. I know how to use a real debugger but adding print/console.log etc. keeps me from breaking context and flow.
An in-circuit emulator was unavailable, so stepping through with a debugger was also not an option.
I ended up figuring out a way to be able to poke values into a few unused registers in an ancillary board within the system, where I could then read the values via the debug port on that board.
So I would figure out what parts of the serial comms code I wanted to test and insert calls that would increment register addresses on the ancillary board. I would compile the code onto a pair of floppy disks, load up the main CPU boards and spend between five and ninety minutes triggering redundancy changeovers until one of the serial ports shat itself.
After which I would probe the registers of the corresponding ancillary board to see which register locations were still incrementing and which were not, telling me which parts of the code were still being passed through. Study the code, make theories, add potential fixes, remove register increments and put in new ones, rinse and repeat for two weeks.
What print debugging and debuggers have in common, in contrast to other tools, is that they can extract data specific to your program (e.g values of variables and data structures) that your program was not instrumented to export. It's really a shame that we generally don't have this capability for production software running at scale.
That's why I'm working on Side-Eye [1], a debugger that does work in production. With Side-Eye, you can do something analogous to print debugging, but without changing code or restarting anything. It uses a combination of debug information and dynamic instrumentation.
Which in some cases I see as related to a sort of macho attitude in programming where people are oddly proud of forgoing using good tooling (or anything from the 21st century really).
There's a section of an interview with John Carmack (https://youtu.be/tzr7hRXcwkw) where he laments the same thing. It's what the Windows/game development corner of the programming world actually got right, people generally use effective tools for software development.
I remember the good old days when I was first learning programming with Applesoft BASIC where print debugging was all there was, and then again in my early days of 8051 programming when I didn't yet have the sophisticated 8051 ICE equipment to do more in depth debugging. Now with the ARM Cortex chips I most often program and their nice SWD interface, print debugging isn't usually necessary. But I still use it occasionally over a serial line because it is simple and why not?
The closest between the two is a logging breakpoint, but the UI for them is generally worse than the UI of the main editor and the logging breakpoint has the same weakness as regular print calls, i.e. you've turned the data into a string and can therefore no longer inspect the objects in the trace.
What I would expect from a debugger in IntelliJ is that when you set a logging breakpoint, then the editor inserts the breakpoint logic source code directly inline with the code itself, so that you can pretend that you are writing a print call with all the IDE features, but the compiler never gets to see that line of code.
1. If a breakpoint debugger exists for the stack, it should still be convenient and configured, and the programmer should have some experience using it. It's a skill/capability that needs to be in reserve.
2. The project has automatic protections against leftover statements being inadvertently merged into a major branch.
3. The dev environment allows loading in new code modules without restarting the whole application. Without that, someone can easily get stuck in rather long test iterations, especially if #1 is not satisfied and "it's too much work" to use another approach.
Often I have to debug bugs I can't reproduce. If method 1 - staring at the code - doesn't work, then it's add print/log statements and send it to the user to test. Repeat until you can reproduce the bug yourself or you fixed it.
There is nothing bad about print debugging, there is no reason to avoid it if that's what works with your workflow and tools. The real question is why you are using print and not something else. In particular, what print does better than your purpose-built debugger? If the debugger doesn't get used, maybe one should look down on that particular tool and think of ways of addressing the problem.
I see many comments against print debugging that go around the lines of "if you learn to use a proper debugger, that's so much better". But in many modern languages that's actually the problem, you have to invest a lot of time and effort on something that should be intuitive. I remember when I started learning programming, with QBasic, Turbo Pascal, etc... using the debugger was the default, and so intuitive I used a debugger before even knowing what a debugger was! And it was 90s tech, now we have time travel debugging, hot reloading, and way more capable UIs, but for some reason, things got worse, not better. Though I don't know much about it, it seems the only ones who get it right are in the video game industry. The rest tend to be stuck with primitive print debugging.
And I say "primitive" not because print debugging is bad in general, but because if print debugging was really to be embraced, it could be made better. For example by having dedicated debug print functions, an easy way to access and print the stack trace, generic object print, pretty printers, overrides for accessing internal data, etc... Some languages already have some of that, but often stopping short of making print debugging first class. Also, it requires fast compilation times.
Complicated setup, slow startup, separate custom UI for adding watches and breakpoints.
Make a debugger integrated with the language and people will use it.
You can then pile up on it subsequent useful features but you have to get basic UI right first. Because half of programmers now are willing to give up stepping, tree inspection even breakpoints just to avoid dealing with the crappy UI of debuggers.
(defmethod move ((a-ship object) (a-place port))
; do something
)
(defmethod move :around (what where)
(print `(start moving object ,what to ,where))
(call-next-method))
Above prints the list to the REPL. The REPL prints the list as data, with the objects WHAT and WHERE included. It remembers that a specific printed output is caused by some object. Later these objects can be inspected or one can call functions on them...This combines print debug statements with introspection in a read-eval-print-loop (REPL).
Writing the output as :before/:around/:after methods or as advise statements, makes it later easier to remove all print output code, without changing the rest of the code. -> methods and advises can be removed from the code at runtime.
For example, I rarely used a debugger in my career as an Android driver developer (mostly C), for several reasons.
1. My first step when debugging is looking at the code to build working hypotheses of what sort of issues could be causing the incorrect behavior that is observed.
2. I find assertions to be a great debugging tool. Simply add extra assertions in various places to have my expectations checked automatically by the computer. They can typically unwind the stack to see the whole call trace, which is very useful.
3. Often, there only choice was command-line GDB, which iI found much slower than GUI debuggers.
4. Print statements can be placed inside if statements, so that you only print out data when particular conditions occur. Debuggers didn't have as much fine control.
5. Debugging multi threaded code. Prints were somewhat less likely to interfere with race conditions. I sometimes embedded sleep() calls to trigger different orderings to occur.
Print debugging was pretty useless back then because compilation took minutes (a full compile took over an hour) rather than milliseconds. If your strategy was "try something, add a print, compile, try something else, add a print, compile" then you were going to have a very bad time.
People working on modern, fast-dev-cycle, interpreted languages today have it easy. You don't know the terror of looking at your code, making sure you have thought of "everything that you're going to need to debug that problem" and hitting compile, knowing that you'll know after lunch whether you have enough debugging information included. I'm sure it was even worse in the punch card era!
This is similar to the "debug f-strings" introduced in python 3.8: print(f"{foo=}"). But it's much easier to type dump(foo) and you get prettier output for complex types.
x = 3
foo = dict(bar=1, baz=dict(hello="world"))
dump(x)
dump(foo)
# prints...
x: 3
foo:
{
"bar": 1,
"baz": {
"hello": "world"
}
}
https://github.com/Aider-AI/aider/blob/main/aider/dump.pyI mostly write Zig these days (love it) and the main thing I'm working on is an interactive program. So the natural way to test features and debug problems is to spin the demo program up and provide it with input, and see what it's doing.
The key is that Zig has a lazy compilation model, which is completely pervasive. If a branch is comptime-known to be false, it gets dropped very early, it has to parse but that's almost it. You don't need dead-code elimination if there's no dead code going in to that phase of compilation.
So I can be very generous in setting up logging, since if the debug level isn't active, that logic is just gone with no trace. When a module starts getting noisy in the logs, I add a flag at the top `const extra = false;`, and drop `if (extra)` in front of log statements which I don't need to have printing. That way I can easily flip the switch to get more detail on any module I'm investigating. And again, since that's a comptime-known dead branch, it barely impacts compiling, and doesn't impact runtime at all.
I do delete log statements where the information is trivial outside of the context of a specific thing I'm debugging, but the gist of what I'm saying is that logging and print debugging blend together in a very nice way here. This approach is a natural fit for this kind of program, I have some stubs for replacing live interaction with reading and writing to different handles, but I haven't gotten around to setting it up, or, as a consequence, firing up lldb at any point.
With the custom debug printers found in the Zig repo, 'proper' debugging is a fairly nice experience for Zig code as well, I use it heavily on other projects. But sometimes trace debugging / print debugging is the natural fit to the program, and I like that the language makes it basically free do use. Horses for courses.
I do think that it’s worth learning your debugger well for programming environments that you use frequently.
In particular, I think that the debugger is exceptionally important vs print debugging for C++. Part of this is the kinds of C++ programs that exist (large, legacy programs). Part of this is that it is annoying to e.g. print a std::vector, but the debugger will pretty-print it for you.
I wrote up a list of tips on how to use gdb effectively on C++ projects awhile back, that got some discussion here: https://news.ycombinator.com/item?id=41074703
It is tricky. I understand why people have a bad experience with gdb. But there are ways to make it better.
I do print debugging most of the times, together with reasoning and some understanding what the code does (!), and I'm usually successful and quick enough with it.
The point here is: today's Internet, with all the social media stuff, is an attention economy. And also some software developers try to get their piece of the cake with extreme statements. They then exaggerate and maximally praise or demonize something because it generates better numbers on Twitter. It'd as simple as that. You shouldn't take everything too seriously. It's people crying for more attention.
We are working on a system that could have nearly total visibility, down to showing us a simulation of individual electrons moving through wires, yet we're programming basically blind. The default is I write code and run it, without any visual/intuitive feedback about what its doing besides the result. So much of my visual system goes completely unused. Also debuggers can be a pain to set up, way more reading and typing than "print()"
It's often faster and easier to set things up to run test cases fast and drop some prints around. Then if there's too much unimportant stuff or something else you want to check on, just switch around the prints and run it again.
That said, I use print debugging all of the time. It is simply more practical in many cases.
It's always so weird to switch to another language which DOES have a debugger...
> import IPython; IPython.embed()
That'll drop you into an interactive shell in whatever context you place the line (e.g. a nested loop inside a `with` inside a class inside a function etc).
You can print the value, change it, run whatever functions are visible there... And once you're done, the code will keep running with your changes (unless you `sys.exit()` manually)
Delete your debug cruft!
Print debugging is how you make software talk back to you. Having software do that is an obvious asset when trying to understand what it does.
There are many debugging tools (debuggers, sanitisers, prints to name a few), all of them have their place and could be the most efficient route to fixing any particular bug.
A debugger gives you insight into the context of a particular code entity - expression, function, whatever.
Seems silly to be dogmatic about this. Both techniques are useful!
https://news.ycombinator.com/item?id=42146864
from "Seer: A GUI front end to GDB for Linux" (15.11.2024)
print Debugging is my main goto. Plus I have no shame :)
I have this in buffer 'd' on vim and Emacs ready for use:
fprintf(stderr, "DEBUG %s %d -- \n", __FILE__, __LINE__); fflush(stderr);
Also on the FE it's often much easier to just console.log than to set breakpoints in the sources in your browser.
Create a debugger that has easily accessible history of execution and we can talk.
The data streams of course can be simulated, then "true" debugging with breakpoints and watches becomes practical, but the simulation is never 100% and getting it close to 100% is sometimes harder than debugging the app out using print debugging. So with most of the code, i only use debugger to analyse crash dumps.
Looking at the comments here, I'm going to have to try to figure out how to use a debugger in pycharm on Monday!
Any tips or good videos on this?
Related
Profiling with Ctrl-C
Ctrl-C profiling is an effective method for identifying performance issues in programs, especially in challenging environments, despite its limitations in sampling frequency and multi-threaded contexts.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Local Variables as Accidental Breadcrumbs for Faster Debugging
The blog post highlights the significance of local variables in debugging, emphasizing their role in providing context for errors, and suggests improvements for error tracking tools like Bugsink.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Caveman Debugging in the Modern Age
Caveman debugging uses print statements to track code execution. Integrating it with IntelliJ IDEA's Live Template feature enhances productivity through custom templates, streamlining repetitive tasks and improving coding efficiency.