Logging Python Tutorial




Welcome to part 22 of the intermediate Python programming tutorial series. We're going to be covering logging in Python .

The idea of Python's logging module is to make your life easier. Have you ever maybe added print functions/statements in your code at various places to note where your code is and what's running? Then maybe commented it out? Maybe you got fancy and created a constant like debug = True, then had some lines like if debug: print(something). If this sounds like you, or if it sounds like something that you might find useful, chances are, you should be using logging instead. Maybe you haven't yet needed to do such things, but, eventually you may find yourself in a situation where you do want to temporarily track what your code is doing, but not always. Using print can help in a pinch, but your life will be easier in the long run if you bake logging into your code early on. With logging, you can both output to console as well as saving to a file, all depending on varying debugging levels you can choose from.

To use logging, you start by importing it, and then setting the basic config:

import logging

logging.basicConfig(level=logging.INFO)

In this case, we're saying we want information on anything from INFO or higher in importance. Your choices are:

DEBUG	Detailed information, typically of interest only when diagnosing problems.
INFO	Confirmation that things are working as expected.
WARNING	An indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The software is still working as expected.
ERROR	Due to a more serious problem, the software has not been able to perform some function.
CRITICAL	A serious error, indicating that the program itself may be unable to continue running.

You can also add in your own levels and ranking if you like. Check out the logging documentation if you want to do this.

This will just output to the console, but you can also save the output to a file, which can be helpful for automated processes, as well as for things like creating logfiles that your users could possibly send to you if they experience issues with your program:

import logging

logging.basicConfig(filename='logfile.log',level=logging.INFO)

To show some examples of logging in use, we're going to use the code that we've been using up to this point in this series. Feel free to apply this to your own code, or the provided code.

blobworld.py:

import pygame
import random
from blob import Blob
import numpy as np

STARTING_BLUE_BLOBS = 15
STARTING_RED_BLOBS = 15
STARTING_GREEN_BLOBS = 15

WIDTH = 800
HEIGHT = 600
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
RED = (255, 0, 0)

game_display = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Blob World")
clock = pygame.time.Clock()

class BlueBlob(Blob):
    
    def __init__(self, x_boundary, y_boundary):
        Blob.__init__(self, (0, 0, 255), x_boundary, y_boundary)

    def __add__(self, other_blob):
        if other_blob.color == (255, 0, 0):
            self.size -= other_blob.size
            other_blob.size -= self.size
            
        elif other_blob.color == (0, 255, 0):
            self.size += other_blob.size
            other_blob.size = 0
            
        elif other_blob.color == (0, 0, 255):
            pass
        else:
            raise Exception('Tried to combine one or multiple blobs of unsupported colors!')
            
        
class RedBlob(Blob):
    def __init__(self, x_boundary, y_boundary):
        Blob.__init__(self, (255, 0, 0), x_boundary, y_boundary)


class GreenBlob(Blob):
    def __init__(self, x_boundary, y_boundary):
        Blob.__init__(self, (0, 255, 0), x_boundary, y_boundary)


def is_touching(b1,b2):
    return np.linalg.norm(np.array([b1.x,b1.y])-np.array([b2.x,b2.y])) < (b1.size + b2.size)

def handle_collisions(blob_list):
    blues, reds, greens = blob_list
    new_blob_dicts = []
    for blue_id, blue_blob in blues.copy().items():
        for other_blobs in blues, reds, greens:
            for other_blob_id, other_blob in other_blobs.copy().items():
                if blue_blob == other_blob:
                    pass
                else:
                    if is_touching(blue_blob, other_blob):
                        blue_blob + other_blob
                        if other_blob.size <= 0:
                            del other_blobs[other_blob_id]
                        if blue_blob.size <= 0:
                            del blues[blue_id]
                            
    return blues, reds, greens
                     
def draw_environment(blob_list):
    game_display.fill(WHITE)
    blues, reds, greens = handle_collisions(blob_list)
    for blob_dict in blob_list:
        for blob_id in blob_dict:
            blob = blob_dict[blob_id]
            pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size)
            blob.move()
            blob.check_bounds()

    pygame.display.update()
    return blues, reds, greens

def main():
    blue_blobs = dict(enumerate([BlueBlob(WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)]))
    red_blobs = dict(enumerate([RedBlob(WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)]))
    green_blobs = dict(enumerate([GreenBlob(WIDTH,HEIGHT) for i in range(STARTING_GREEN_BLOBS)]))

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()
        blue_blobs, red_blobs, green_blobs = draw_environment([blue_blobs,red_blobs,green_blobs])
        clock.tick(60)

if __name__ == '__main__':
    main()

blob.py:

import random


class Blob:

    def __init__(self, color, x_boundary, y_boundary, size_range=(4,8), movement_range=(-1,2)):
        self.size = random.randrange(size_range[0],size_range[1])
        self.color = color
        self.x_boundary = x_boundary
        self.y_boundary = y_boundary
        self.x = random.randrange(0, self.x_boundary)
        self.y = random.randrange(0, self.y_boundary)
        self.movement_range = movement_range

    def move(self):
        self.move_x = random.randrange(self.movement_range[0],self.movement_range[1])
        self.move_y = random.randrange(self.movement_range[0],self.movement_range[1])
        self.x += self.move_x
        self.y += self.move_y

    def check_bounds(self):
        if self.x < 0: self.x = 0
        elif self.x > self.x_boundary: self.x = self.x_boundary
        
        if self.y < 0: self.y = 0
        elif self.y > self.y_boundary: self.y = self.y_boundary

Now, in general, any operation that might have a LOT of output should be DEBUG level only. If you're outputting to a console, doing a lot of outputting will cost processing. If you're logging to a file, it's even more imperative to be careful, since, logfiles can quickly grow and get out of hand, unbenknownst to the user. For example, on two occasions, I had MySQL log files that got so large that they exhausted all of my storage space, then, on new insert statements, the server would fail, and then, from here, the MySQL server can just entirely stop, and you won't even be able to connect. When that happens, it's not immediately clear that the real issue is you ran out of space. Anyway, that's just one example from my experience, back to the tutorial.

After picking the level of warning that you want, you can throw in logging wherever you please. For example, maybe we want to have just a simple informative notification whenever blobs are interacting with each other. We can modify the __add__ method:

    def __add__(self, other_blob):
        logging.info('Blob add op: {} + {}'.format(str(self.color), str(other_blob.color)))
        if other_blob.color == (255, 0, 0):
            self.size -= other_blob.size
            other_blob.size -= self.size
            
        elif other_blob.color == (0, 255, 0):
            self.size += other_blob.size
            other_blob.size = 0
            
        elif other_blob.color == (0, 0, 255):
            pass
        else:
            raise Exception('Tried to combine one or multiple blobs of unsupported colors!')

The message we put in here will be output to the console (assuming you've imported logging). After importing logging, you can begin using it, and the default logging level is WARNING. You can change it with logging.basicConfig(level=logging.DEBUG) for example.

To exemplify a fast-outputting DEBUG-only level task, let's modify a line in our handle_collisions function:

def handle_collisions(blob_list):
    blues, reds, greens = blob_list
    new_blob_dicts = []
    for blue_id, blue_blob in blues.copy().items():
        for other_blobs in blues, reds, greens:
            for other_blob_id, other_blob in other_blobs.copy().items():
                logging.debug('Checking if blobs touching {} + {}'.format(str(blue_blob.color), str(other_blob.color)))
                if blue_blob == other_blob:
                    pass
                else:
                    if is_touching(blue_blob, other_blob):
                        
                        blue_blob + other_blob
                        if other_blob.size <= 0:
                            del other_blobs[other_blob_id]
                        if blue_blob.size <= 0:
                            del blues[blue_id]

In that final for loop, the first line is a debug-level log. Set the debugging level to DEBUG: logging.basicConfig(level=logging.DEBUG), and go ahead and run it. It should be a bunch of output like:

DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 255, 0)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
INFO:root:Blob add op: (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)
DEBUG:root:Checking if blobs touching (0, 0, 255) + (0, 0, 255)

Let's raise the debugging to INFO: logging.basicConfig(level=logging.INFO). Now, once in a while, you should get: INFO:root:Blob add op: (0, 0, 255) + (255, 0, 0), but that's it. Now, let's add a logging file to log to: logging.basicConfig(filename='logfile.log',level=logging.INFO)

Now, rather than outputting to console, you will get the logging data sent to the file.

In the next tutorial, we're going to cover how to better handle and save information on errors when you're running programs automatically.

The next tutorial:





  • Intermediate Python Programming introduction
  • String Concatenation and Formatting Intermediate Python Tutorial part 2
  • Argparse for CLI Intermediate Python Tutorial part 3
  • List comprehension and generator expressions Intermediate Python Tutorial part 4
  • More on list comprehension and generators Intermediate Python Tutorial part 5
  • Timeit Module Intermediate Python Tutorial part 6
  • Enumerate Intermediate Python Tutorial part 7
  • Python's Zip function
  • More on Generators with Python
  • Multiprocessing with Python intro
  • Getting Values from Multiprocessing Processes
  • Multiprocessing Spider Example
  • Introduction to Object Oriented Programming
  • Creating Environment for our Object with PyGame
  • Many Blobs - Object Oriented Programming
  • Blob Class and Modularity - Object Oriented Programming
  • Inheritance - Object Oriented Programming
  • Decorators in Python Tutorial
  • Operator Overloading Python Tutorial
  • Detecting Collisions in our Game Python Tutorial
  • Special Methods, OOP, and Iteration Python Tutorial
  • Logging Python Tutorial
  • Headless Error Handling Python Tutorial
  • __str__ and __repr_ in Python 3
  • Args and Kwargs
  • Asyncio Basics - Asynchronous programming with coroutines