Stop Using "print" for Debugging: A 5 Minute Quickstart Guide to Python’s logging Module

  • This tutorial is short.
  • To figure out bugs in your code, you might put in print statements/print() calls to display the value of variables.
  • Don’t do this. Use the Python logging module.

The logging is better than printing because:

  • It's easy to put a timestamp in each message, which is very handy.
  • You can have different levels of urgency for messages, and filter out less urgent messages.
  • When you want to later find/remove log messages, you won't get them confused for real print() calls.
  • If you just print to a log file, it's easy to leave the log function calls in and just ignore them when you don't need them. (You don't have to constantly pull out print() calls.)

Using print is for coders with too much time on their hands. Use logging instead. Also, learn to use the Python debugger to debug bugs and Pylint to prevent bugs and make your code readable.

To print log messages to the screen, copy and paste this code:

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('This is a log message.')

To write log messages to a file, you can copy and paste this code (the only difference is in bold):

import logging
logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('This is a log message.')

Later runs of the program will append to the end of the log file, rather than overwrite the file.

To log messages to a file AND printed to the screen, copy and paste the following:

import logging
logger = logging.getLogger()

formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

fh = logging.FileHandler('log_filename.txt')

ch = logging.StreamHandler()

logger.debug('This is a test log message.')

Make sure that the logger variable is global, so that you can use it in functions. (You don't need the "global logger" at the top of the function, because the logger variable is only read, not modified.)

The different levels of logging, from highest urgency to lowest urgency, are:

  2. ERROR
  4. INFO
  5. DEBUG

The setLevel() call sets the minimum log level of messages it actually logs. So if you fh.setLevel(logging.ERROR), then WARNING, INFO, and DEBUG log messages will not be written to the log file (since fh is the log handler for the log file, as opposed to ch which is the handler for the console screen.)

To write a log message in one of these five levels, use the following functions:

  1. logger.critical('This is a critical message.')
  2. logger.error('This is an error message.')
  3. logger.warning('This is a warning message.')
  4.'This is an informative message.')
  5. logger.debug('This is a low-level debug message.')

There's plenty more you can do, but this all you need to know to never again use print() calls to do your debugging work.

The Python documentation has more info, including a Basic Tutorial, an Advanced Tutorial, and a Logging Cookbook.

Also, the pprint.pprint() function is great for "pretty printing" dictionaries and lists that have nested dictionaries and lists in them. The pprint.pformat() function returns the string of this content, rather than printing it to the screen.

One final tip: You can use the tail -f logfile.txt command to show a file as it is being written to. The -f stands for "follow". Just leave a terminal/console window open with this command running, and new text in the log file will appear as it is written. This way, you don't have to keep opening/reloading a text editor to view the latest text in the log file.

The tail command comes on Mac OS X and Linux OSes. On Windows, you can download the Cygwin project to get the tail command.

19 thoughts on “Stop Using "print" for Debugging: A 5 Minute Quickstart Guide to Python’s logging Module

    1. If you'll actually read the article, there's only two lines to paste to have a logger that prints to the screen (just like print does). A small change and it can print to a file instead. The 12 lines (not 30) to do both isn't that bad (especially since you can copy and paste it from this blog, and the 3 setLevel() lines aren't even needed by default if you want to log everything.)

      The short-sightness and exaggeration are the exact same things that kept me from learning how to do things in a new (and more efficient) way. Grow out of it.

  1. While I don't agree with LEW21's sarcasm, your third example - logging to a file and to the screen - underscores a design problem with the logging module. If you are logging to the screen OR a file, it takes one call to configure the logger. But if you are logging to both it's quite a bit more complicated. It should only take two calls.

    I have my own module to simplify my use of logging in my projects, but I think the need to wrap a standard library module in your own is a code smell. And that's why I think it's so common to see people using print instead of logging.

  2. This looks very useful, bookmarking immediately. I was just up against a problem the other day and a coworker and I were talking about adding logging to troubleshoot. I'll be digging in to this over the weekend.

  3. Great article! I must admit I'm guilty of littering my code with endless print statements which I later have to hastily remove before committing.

    This is quick, simple and concise. Thanks!

  4. well, what if you are logging strings wich require a certain amount of CPU to format ? ( ex : logging( json.dumps(big_complex_dict))
    Will you get rid of this CPU burden by disabling the logging function ?

    I guess not.

    One should still use logging sparsely, and clean the code after debugging*.

    (*However, I personnaly keep silent logging function in some code as both a warning there is a caveat (and a practival HOWTO debug) and sometimes as a comment in tricky parts of code. )

  5. @jul,

    "well, what if you are logging strings wich require a certain amount of CPU to format ? ( ex : logging( json.dumps(big_complex_dict))
    Will you get rid of this CPU burden by disabling the logging function ?"

    Yes, beyond a shadow a doubt. If it doesn't you've misunderstood the meaning of the word "disable".

    Debugging code is just a cost of doing business. Either write it perfectly the first time or get used to looking a debug log when you're testing it.

  6. I agree with David Avraamides that these examples illustrate that the API of the logging module right now isn't very good. For example:

    import logging

    This displays only the message from the error call (fair enough). But now I want to enable the debug messages. Can I simply do 'logging.setLevel(Logging.DEBUG)'? Answer no.

    A good API should at least make the effort involved in moving from trivial stuff to a very slightly higher level proportionate with the change, rather than requiring you to understand a lot of implementation detail.

    Even the basic facility of timestamping is missing by default, the result is that using print is actually easier in general. A simple module that offers debug(), error() etc. methods actually feels much more attractive than the basic logging facility. At least if you avoid using print directly you can replace it with some magic from logging later.

    I'd say the basic message here is 'use a wrapper function around print', and maybe use logging later. That's not how it should be of course, just how it seems to me right now.

  7. But is there some way by which we can filter out the log messages from different modules, and allow only those from modules we want?

  8. Logging to a file is practically impossible in LoseThos. When I made my native file system, my one goal was not to get sued, like FAT32. I did not make a fat table, just used an allocation bitmap. Files must be contiguous and you cannot grow them. Technically you can if you use a FAT file system, but I try to avoid anything which works on FAT and not my native system. The is no FPRINTF().

  9. In your last example, are you sure you need to have `logger` as a global variable? It seems to me that the function `getLogger` returns the root logger of the application as a singleton (every time you call it, it'll return a reference to the same object).

    >>> import logging
    >>> logging.getLogger()

    >>> logging.getLogger()

    `getLogger` documentation:

  10. You can use
    if logger.isEnabledFor(logging.DEBUG):

    to avoid expensive computations in production

  11. One way to reduce the amount of code you need to copy and paste in every new source code file is to use the logging.config functionality to create a logging settings file that is shared by all of your modules. Then you can setup as complicated of logging as you want, or edit the log settings once and have it be applied to everything you've already written with no code changes. Here is the code I use in my source to load the settings:

    if __name__=="__main__":
    import logging.config
    import logging
    logger = logging.getLogger(__name__)

  12. Thanks! I've always done logging to file and console with Java using log4j or similar, but since I'm having to write Python lately, it's nice to make the code a little more professional. Timestamped, formatted logging is indispensable for production code.

  13. I've only just started working in python within the past 4-6 months, coming from a background in shell scripting and some reeeeeally old experience with C/C++. I started working on a project with a coworker, and noticed that import logging was the first line following the shebang in his first pass over the code.

    "That's cool," thinks myself. "I did want to implement some better logging into this program. I'll look that up once I get done adding in the core functions." And then I proceeded to write the majority of the core functionality of the project, using print statements to debug.

    I just spent about an hour rejiggering everything to only use print for - y'know - the stuff I want to print in production, and use logging for everything else. Bless you for this post - it made things make much more sense almost immediately.

    I'm still playing with it a bit - I've written a logging_init() function that I can port from file to file (eventually, I'll write a module to import, but since I'm still tinkering with the code, a function has made more sense) to allow me to specify the log levels for file and console separately, so once I'm done testing a specific section, I just change a variable passed to the function and I'm no longer flooded with debug messages. I'm sure it's just the beginning, so thanks for getting me rolling!

Leave a Reply

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