DEV Community

Cover image for Using Print Statements Are A Handy Way to Debug and Explore Code
Nick Janetakis
Nick Janetakis

Posted on • Originally published at nickjanetakis.com

Using Print Statements Are A Handy Way to Debug and Explore Code

This article was originally posted on Sep 9th 2018 at: https://nickjanetakis.com/blog/using-print-statements-are-a-handy-way-to-debug-and-explore-code


In the past I've written about rubber duck debugging which is one way to help solve problems. Rubber duck debugging is handy because it forces you to explain the problem in great detail which gets you to break it down.

But now let's say you know what you want to do and you're trying to implement it. Using print statements can help you go from non-working code to working code very quickly.

Why Bother Using Print Statements?

When writing software, knowledge really is power. The more you know about what's happening, the better. Dropping in print statements is a quick way see what's happening. This isn't even limited to debugging problems too. It's useful for general code insight.

That's why when I write code that doesn't work on the first attempt, I skip trying to be a hero and just start dropping in print statements everywhere. The faster I discover what's wrong, the faster I'll be able to move onto the next thing.

When Are Using Print Statements Useful?

Here's 3 situations where I find myself using print statements:

  • When you want to quickly see the value of something
  • Trying to use a divide and conquer approach in a longer function
  • Exploring new code bases and you want some help tracing the code base

Quickly See the Value of Something

One of the biggest lessons I've learned over the years is not to assume anything. If you have even an ounce of doubt on what something might be, then print it out to see for sure.

It only takes a few seconds to print it out and confirm. That could save you many many minutes of guesswork (which leads to hours due to getting distracted on Youtube).

Divide and Conquer

In a previous article about microservices I talked about levels of code abstractions.

When dealing with a fairly complicated piece of logic, I tend to inline most of the code into a single function until I work out the logic. I just find that style the easiest to work with.

The problem is hard enough as it is, why bother complicating it by worrying about naming functions and refactoring it as I go before I even understand the problem or have a solution written?

What this usually means is I'll end up with pretty long functions to begin with. It's not uncommon to write a 30 or 40 line function on a first pass.

Sometimes you're not even sure if certain lines of a condition or a piece of code is getting executed, but dropping in a print statement is a very quick way to determine what path of the code is being executed.

It could be as simple as dropping print("Is this being executed?") somewhere.

Exploring New Code Bases

As a freelance developer I'm often tasked with doing something to an existing code base. This could be adding new features, fixing bugs or adding tests. It could be anything.

These applications have a huge range in complexity. Some of them are 100 line Flask apps while others are 100,000+ line Rails apps, but one common trend I find is I rely on printing things to quickly trace a new code base.

Once you get familiar enough with your web framework of choice you have a general idea of when code gets ran based on where it exists, but still, this wildly changes depending on what you're working on, especially if there's a lot of meta programming to unwind.

A better example of divide and conquer:

You can't control the quality of code you didn't write.

Sometimes you end up in situations where you're forced to figure out what the heck a 400 line function is doing and of course the person who wrote it is long gone.

What I like to do in these spots is eliminate as much code as possible from the equation in the quickest amount of time possible, then chunk it out.

For example if you drop in a print statement on line 200 and you see it being output then you know for sure at least the first 199 lines are running. You might want to output the value of some variable on line 200.

Then you can output that variable again on line 100 and if the value of that variable is correct on line 100 but incorrect on line 200, you can safely conclude that something is messed up in between lines 101 and 199.

Now you can pop in a print statement on line 150 and check out the variable. If it's correct then you know the problem is in between lines 151 and 199, or if it's not correct then you know the problem is somewhere between lines 101 and 149.

This form of using debugging is called bisection search, or binary search depending on where you learned it from. Using print statements in a bisection search fashion lets you ignore code with 100% confidence allowing you to get to the root of the problem quickly.

Quickly Finding Your Print Statements

A lot of web frameworks have pretty verbose logging in development, and that's good. As we talked about before, in some cases having more information is better.

But this also means it can be really difficult to find your print statements. Here's some log output from a Dockerized Phoenix application I happen to be working on at the moment:

web_1       | [info] GET /
web_1       | [debug] QUERY OK source="users" db=1.7ms
web_1       | SELECT u0."id", u0."admin", u0."confirmed_email_at", u0."deactivated_at", u0."email", u0."email_auth_token", u0."name", u0."profile_hexcolor", u0."professional_title", u0."signed_in_at", u0."profile_photo", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [33]
web_1       | [debug] Processing with LMSWeb.PageController.index/2
web_1       |   Parameters: %{}
web_1       |   Pipelines: [:browser]
web_1       | [info] Sent 200 in 3ms
Enter fullscreen mode Exit fullscreen mode

Nothing too crazy here, this is just rendering the home page, but let's say we had a print statement thrown into the mix:

web_1       | [info] GET /
web_1       | [debug] QUERY OK source="users" db=1.7ms
web_1       | SELECT u0."id", u0."admin", u0."confirmed_email_at", u0."deactivated_at", u0."email", u0."email_auth_token", u0."name", u0."profile_hexcolor", u0."professional_title", u0."signed_in_at", u0."profile_photo", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [33]
web_1       | Hi
web_1       | [debug] Processing with LMSWeb.PageController.index/2
web_1       |   Parameters: %{}
web_1       |   Pipelines: [:browser]
web_1       | [info] Sent 200 in 3ms
Enter fullscreen mode Exit fullscreen mode

At a quick glance, finding our print message in that isn't easy and we're only dealing with a few lines of log output here. Imagine if you had 5x the amount of lines which took up your entire terminal buffer.

Compare that to:

web_1       | [info] GET /
web_1       | [debug] QUERY OK source="users" db=1.7ms
web_1       | SELECT u0."id", u0."admin", u0."confirmed_email_at", u0."deactivated_at", u0."email", u0."email_auth_token", u0."name", u0."profile_hexcolor", u0."professional_title", u0."signed_in_at", u0."profile_photo", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [33]
web_1       | ------------------------------------------------------- Hi
web_1       | [debug] Processing with LMSWeb.PageController.index/2
web_1       |   Parameters: %{}
web_1       |   Pipelines: [:browser]
web_1       | [info] Sent 200 in 3ms
Enter fullscreen mode Exit fullscreen mode

Now we can easily see the "Hi" message in a second. Our eye is immediately drawn to it.

For single lines of output I tend to always prefix the message with a bunch of characters. I don't always use -. Really, it doesn't matter as long as you can see it.

Sometimes if I'm outputting a few variables, I'll use different characters for each variable. Perhaps --------------- and ~~~~~~~~~~~~~~~. If it's the same variable on multiple lines I'll include a --------------- L150: or whatever makes sense.

If I plan to output a few lines of text, I'll sometimes gate them, such as:

web_1       | [info] GET /
web_1       | [debug] QUERY OK source="users" db=1.7ms
web_1       | SELECT u0."id", u0."admin", u0."confirmed_email_at", u0."deactivated_at", u0."email", u0."email_auth_token", u0."name", u0."profile_hexcolor", u0."professional_title", u0."signed_in_at", u0."profile_photo", u0."username", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [33]
web_1       | -------------------------------------------------------
web_1       | Name: Foo
web_1       | Email: foo@bar.com
web_1       | -------------------------------------------------------
web_1       | [debug] Processing with LMSWeb.PageController.index/2
web_1       |   Parameters: %{}
web_1       |   Pipelines: [:browser]
web_1       | [info] Sent 200 in 3ms
Enter fullscreen mode Exit fullscreen mode

So that's how I approach print statement based debugging. It's a well used tool in my programming toolbelt.

What tips do you have for debugging with print statements? Let me know below!

Top comments (17)

Collapse
 
yaser profile image
Yaser Al-Najjar • Edited

I still remember once I said in reddit:

I don't use the built-in debugger in Visual Studio 2017

I got lots of negative attitude like:

Are you really doing software developing?!

Heck yeah... I generally use Console.WriteLine or print for debugging my apps!


To make finding the printed line easier for me (say I wanna see the value of x), I do something like:

print('koko')
print(x)

So I'm gonna search for the word "koko" in the output, and the next line should be x :D

Collapse
 
samuraiseoul profile image
Sophie The Lionhart

Yeah I'm going to have to agree with them for the most part.

This is fine for a quick "Does it hit this line?" or "What is the value of this one thing?" But from there you need more diagnostics. What caused that value? Now that I know this value, what are the values of these other things, how many times is this gonna loop, why didn't this thing evaluate to true? You might have all of those questions sequentially, and will have to run your program again and again and AGAIN. But if you had a break point, and used your debugger, you could have figured it out in one run. You could have used the console to evaluate some operations on the variable x + 2 is "12" instead of 3 because x is a string, oops. Its easier to see types and things using the debugger than using print.

Collapse
 
yaser profile image
Yaser Al-Najjar

I agree with you... in some cases like doing queries and digging through each the vars in the hierarchy, it's much faster to debug.

But, for checking the type, one simply can just print(type(x)) aside from the fact that exceptions show clearly what happened when you add two different types in python :D

But using the debugger as a frequent used tool is just time consuming.

Thread Thread
 
samuraiseoul profile image
Sophie The Lionhart

That works in python, but for other langs it won't. In addition the debugger should not be any more time consuming than the print statement. Add the break point and run the program, you have to type the print and run the program anyways. If its too hard then you have some other problem in your workflow to work out. You also run the risk of forgotten print statements and logging. enough of those especially in a loop can absolute MURDER performance. Print statements for debugging are no faster than debugging, but they are more dangerous.

Thread Thread
 
yaser profile image
Yaser Al-Najjar

In addition the debugger should not be any more time consuming than the print statement

I developed recently in django and spinning the debugger takes more time than printing and re-running the app.

It always depends, sometime running the shell and trying things out there is really much faster.

Print statements for debugging are no faster than debugging, but they are more dangerous

When you try this stuff in Android development (esp. Xamarin Android), you will change this line :D

enough of those especially in a loop can absolute MURDER performance

Generally you won't test a loop or an algorithm by hand / debugger, you would write logical tests against it... and run those tests till you get green ;)

Collapse
 
bgadrian profile image
Adrian B.G. • Edited

You would love non-suspending breakpoints in the IDE, the effect is the same but you do not modify the code.

Seeing this type of posts in 2018 is staggering, but hey, if it works for you ...

Sometimes you end up in situations where you're forced to figure out what the heck a 400 line function is doing and of course the person who wrote it is long gone.

  1. add tests
  2. refactor it
  3. you will understand it
Collapse
 
nickjj profile image
Nick Janetakis • Edited

Seeing this type of posts in 2018 is staggering, but hey, if it works for you...

I'm just spoiled because debugging was a much better experience 20 years ago with Visual Basic 6. Everything was super integrated.

Nowadays you have web servers, templating languages, databases, backends, frontends, etc.. It's not easy to find a debugging solution that doesn't completely suck, especially if you consider you're running your apps in Docker (and most editors have no idea how to debug code running inside of a container).

Over the years I found it faster (and easier) to just litter in print statements on demand where necessary. That's after building about 100 apps in a bunch of assorted frameworks and languages.

Collapse
 
samuraiseoul profile image
Sophie The Lionhart

I got php remote debugging working with both phpstorm and with vscode if anyone needs help on it. It was an absolute pain in the ass. I'm here for you fam.

Collapse
 
bgadrian profile image
Adrian B.G.

Yes that is true, that is an universal method, cross langauges, envs and editors. But we should make and use better tools. let's improve.

Collapse
 
nssimeonov profile image
Templar++

We all did this. And we know it's not too wrong, although there are better ways - like log files. But writing an article to encourage the young and misguided to use this is a whole new level of wrong. This is how sometimes debug messages slip in production and users can see them. And all modern languages and environments have excellent debug and logging tools, that are thread-safe and may work even across multiple servers.

And yes, I did this too, but I'm not proud of that I would never encourage it.

Collapse
 
dance2die profile image
Sung M. Kim

JavaScript Console has so many methods.

But I use console.table often to quickly see an object in a tabular format (as I learned how from Wes Bos).

I rarely use'em all but Wes has a course on how to print like a Bos 😉.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

What the actual! This is amazing!

Collapse
 
nssimeonov profile image
Templar++

THANK YOU!

Collapse
 
jeikabu profile image
jeikabu

One of the situations where logging is really invaluable is doing any kind of post-mortem analysis. If there's an event you're looking for but you need to know the state from some time before that, investigating issues in production environments, tracking down bugs with really low repro rates, etc.

I've yet to use it, but Visual Studio Code got a "logpoint" feature that caught my eye. Apparently it's also in regular VS (and probably other IDEs), but it was the first time I'd seen it.

Collapse
 
aezore profile image
Jose Rodriguez

I'm mainly a Python programmer for embedded electronics and hex files voodoo so I tend to use a lot of print statements pretty much everywhere while debugging, I used the logging module too but for a more "rock solid" debugging purpose where I expect things to fail. But for the casual quick'n dirty "why the f*** is this not doing what I want" I plague the blamed chunk of code full of prints; once I figure it out I remove them and put a more sensible logging code for next time. So far I haven't got much trouble (if any) although I'm not used to production and all my work is mainly in-house dev and precise problem solving.

I tried the debugger a few times but...since I do a lot of cross-programing (writing code in my computer and running it on a raspberry) modern debuggers are more clumsy than a straight print. I'm still learning on my own so I can only talk about my actual know-how and experience. Maybe someone can point out a better approach?

Collapse
 
rbanffy profile image
Ricardo Bánffy

Did people really forget about this?

Collapse
 
thespiciestdev profile image
James Allen

When in doubt, log it out.