Utilizing the Icecream Package for Efficient Print Debugging & How to Customize It

Utilizing The Icecream Package For Efficient Print Debugging & How To Customize It

Hello! This is Atsushi Yasumoto, a data scientist at HACARUS. 

In this article, I will give an overview of the icecream package, a useful tool for debugging print while using Python. I will also introduce a customized feature we have added at HACARUS as part of our work. Before trying this for yourself, please use pip install icecream to install the v2.1.2 version of icecream. 

To start, let’s look at a quick example of print debugging. Suppose you have an assignment statement of y = x + 1 in a function, and you want to see the result of executing x + 1.

If we simply use print(y), it is difficult to tell what kind of processing results in y. It is also difficult to distinguish if there are other print functions being executed in the script. To account for this, we would write print("x + 1:", y), which is tedious to write each time the debugging point is changed.

However, icecream provides an easy fix to this problem with the icecream.ic function (referred to as ic for the remainder of this article). Using the ic function y = x + 1 can be rewritten simply as y = ic(x + 1). Another advantage of the ic function is that it returns its value as-is, unlike the print function, which has a return value of None. This allows it to be used in the middle of an expression, such as y = ic(x + 1).

Below is a practical example of the ic function. Here, it is important to note that the ic function is used within the f function, and when executed, ic| x + 1: 2 is displayed. 

>>> from icecream import ic
>>>
>>> def f(x):
>>>     y = ic(x + 1)
>>>     return y
>>>
>>> # icecreamでデバッグしながら実行
>>> y = f(1)
ic| x + 1: 2
>>>
>>> # 返り値の確認
>>> print(y)
2

Here, ic uses the pprint.pprint function internally to display the contents of objects in a clear manner. (refer to 「Pythonのpprintの使い方(リストや辞書を整形して出力)」). 

While powerful, there are a few limitations of the pprint.pprint function. Since it is provided by the standard library, there are situations where it is not powerful enough to display instances of classes provided by external modules such as the Numpy Array. For example, when a Numpy Array of 100 rows and 100 columns is put into the ic function, the user can only understand that it contains a large array of real numbers. Unfortunately, in data science, the shape or the distribution of values may be more useful than the object values themselves. 

>>> import numpy as np
>>>
>>> x = np.random.RandomState(0).normal(size=(100, 100))
>>> ic(x)
ic| x: array([[ 1.76405235,  0.40015721,  0.97873798, ...,  1.78587049,
                0.12691209,  0.40198936],
              [ 1.8831507 , -1.34775906, -1.270485  , ...,  0.82350415,
                2.16323595,  1.33652795],
              [-0.36918184, -0.23937918,  1.0996596 , ...,  0.58295368,
               -0.39944903,  0.37005589],
              ...,
              [-0.05524379,  0.13064302,  0.44069106, ...,  0.97220715,
               -0.91895048,  0.6632405 ],
              [-0.1334914 , -1.56637034, -1.74865144, ...,  0.0708476 ,
                2.43572851,  0.9716812 ],
              [-0.93296221,  2.86520354, -1.79204799, ...,  0.51687218,
               -0.03292069,  1.29811143]])

As a way to get around these limitations, at HACARUS, we have implemented a flexible and easy way to customize the ic function. We then proposed it to the head office and it has been adopted within the company.

Earlier, I mentioned that the ic function uses the pprint.pprint function internally. A function called icecream.argumentToString is also used to wrap it. Here, we have added a tool called singledispatch, which selects processing based on the type of the argument. This allows the user to add or remove processing based on the type as well.

We can also add processing by defining a function using the argumentToString.register method as a decorator. Here, the key is to annotate the type with obj: np.ndarray. The ic function will then choose to process the input data according to its type.

For instance, let’s add a function that prints the shape, the dtype, and the minimum and maximum values of an array when a Numpy Array is passed to the ic function.

>>> from icecream import ic, argumentToString
>>> import numpy as np
>>>
>>> # Register a function to summarize numpy array
>>> @argumentToString.register
>>> def argumentToString_ndarray(obj: np.ndarray):
>>>     return (
>>>          f"ndarray, shape={obj.shape}, dtype={obj.dtype}, "
>>>          f"min={obj.min()}, max={obj.max()}"
>>>     )
>>>
>>> ic(x)
ic| x: ndarray, shape=(100, 100), dtype=float64, min=-3.740100637951779, max=3.8016602149671153

Here we have written the process for the Numpy Array in the argumentToString_ndarray function, but there is no restriction on the name. If you do not plan to call the function by name, you can use _ as the name.

Next, to see a list of registered functions by type, let’s refer to the argumentToString.registry property, where the content is an instance of MappingProxyType. It might help to think of it as a read-only version of dict.

>>> print(argumentToString.registry)
mappingproxy({object: <function icecream.icecream.argumentToString(obj)>,
              numpy.ndarray: <function __main__._(obj: numpy.ndarray)>})

To delete a registered function, specify the type of processing to be deleted in the argumentToString.unregister method.

>>> argumentToString.unregister(np.ndarray)

The function implemented this time is based on the functools.singledispatch function (reference 公式ドキュメント). This function can be implemented simply by adding only two lines as follows:

+ from functools import singledispatch
+ @singledispatch
  def argumentToString(obj):

However, the actual Pull Request became a bit more complex at the request of reviewers.

Find the code on Github (https://github.com/gruns/icecream/pull/115).

  • Python 2 does not have functools.singledispatch, so it is not necessary for single dispatching, but I would like the ic functions to remain available.
  • The original functools.singledispatch has a function to register functions but isn’t able to delete them.

In the end, I am surprised that you are still willing to support Python 2. I think it is one of the best parts of OSS development is enjoying the differences in philosophy in this area. The latter was also a problem to implement, but fortunately, I was able to find the right answer on StackOverflow (ドンピシャの回答) and it turned out to be a success.

I hope that all of you are able to enjoy using the icecream package! More information about the package and porting it to other languages can be obtained from the official repository. 

Subscribe to our newsletter

Click here to sign up