Leveraging the Jupyter and IPython display protocol

In python for every object you create there is a __repr__ method in it which is used to display it wherever you print that variable.

class Student:

    def __init__(self, name, class_name) -> None:
        self.name = name
        self.class_name = class_name

    def __repr__(self):
        return f"Hi! I'm {self.name} of {self.class_name}"

if __name__ == "__main__":
    s = Student("totoro", "5")
    print(s) # Output: Hi! I'm totoro of 5

Same in true for Ipython also, but in this case we can give other type of representations like HTML, images, latex.

Example:

class MultiMime:

    def __repr__(self):
        return "this is the repr"

    def _repr_html_(self):
        return "This <b>is</b> html"

    def _repr_markdown_(self):
        return "This **is** markdown"

    def _repr_latex_(self):
        return "$ Latex \otimes mimetype $"

If you run this in jupyter notebook, you will get the _repr_html_ display.

There is a priority on which type of mimetype will be used,

sphinx:
  config:
    nb_mime_priority_overrides: [
      ['html', 'application/vnd.jupyter.widget-view+json', 10],
      ['html', 'application/javascript', 20],
      ['html', 'text/html', 30],
      ['html', 'image/svg+xml', 40],
      ['html', 'image/png', 50],
      ['html', 'image/gif', 60],
      ['html', 'image/jpeg', 70],
      ['html', 'text/markdown', 80],
      ['html', 'text/latex', 90],
      ['html', 'text/plain', 100]
    ]

More details can be found here.

Here is an example of a Gaussian class which has representations in multiple formats.

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

from IPython.core.pylabtools import print_figure
from IPython.display import Image, SVG, Math

class Gaussian(object):
    """A simple object holding data sampled from a Gaussian distribution.
    """
    def __init__(self, mean=0, std=1, size=1000):
        self.data = np.random.normal(mean, std, size)
        self.mean = mean
        self.std = std
        self.size = size
        # For caching plots that may be expensive to compute
        self._png_data = None
        self._svg_data = None

    def _figure_data(self, format):
        fig, ax = plt.subplots()
        ax.plot(self.data, 'o')
        ax.set_title(self._repr_latex_())
        data = print_figure(fig, format)
        # We MUST close the figure, otherwise IPython's display machinery
        # will pick it up and send it as output, resulting in a double display
        plt.close(fig)
        return data

    # Here we define the special repr methods that provide the IPython display protocol
    # Note that for the two figures, we cache the figure data once computed.

    def _repr_png_(self):
        if self._png_data is None:
            self._png_data = self._figure_data('png')
        return self._png_data


    def _repr_svg_(self):
        if self._svg_data is None:
            self._svg_data = self._figure_data('svg')
        return self._svg_data

    def _repr_latex_(self):
        return r'$\mathcal{N}(\mu=%.2g, \sigma=%.2g),\ N=%d$' % (self.mean, self.std, self.size)

    # We expose as properties some of the above reprs, so that the user can see them
    # directly (since otherwise the client dictates which one it shows by default)
    @property
    def png(self):
        return Image(self._repr_png_(), embed=True)

    @property
    def svg(self):
        return SVG(self._repr_svg_())

    @property
    def latex(self):
        return Math(self._repr_latex_())

    # An example of using a property to display rich information, in this case
    # the histogram of the distribution.  We've hardcoded the format to be png
    # in this case, but in production code it would be trivial to make it an option
    @property
    def hist(self):
        fig, ax = plt.subplots()
        ax.hist(self.data, bins=100)
        ax.set_title(self._repr_latex_())
        data = print_figure(fig, 'png')
        plt.close(fig)
        return Image(data, embed=True)

By default if you display a object of this class, it will display the represenation with highest priority. For other representations you can use the property directly

x = Gaussian()
x # shows default one
x.svg # shows svg
x.png # shows png

# a additional usecase
x.hist # shows histogram

You can also register custom formatters for different types of objects. This allows creation of third party libraries like disp, although it has not been updated for a long time. Link for disp repo

Here is how you register custom formatter:

ipython = get_ipython()
html_formatter = ipython.display_formatter.formatters['text/html']

# registering for type list and formatter html_list_formatter
html_formatter.for_type(list, html_list_formatter)