Using the IPython display protocol for your own objects

IPython extends the idea of the __repr__ method in Python to support multiple representations for a given object, which clients can use to display the object according to their capabilities. An object can return multiple representations of itself by implementing special methods, and you can also define at runtime custom display functions for existing objects whose methods you can't or won't modify. In this notebook, we show how both approaches work.


Note: this notebook has had all output cells stripped out so we can include it in the IPython documentation with a minimal file size. You'll need to manually execute the cells to see the output (you can run all of them with the "Run All" button, or execute each individually).

Parts of this notebook need the inline matplotlib backend:

In [1]:
%pylab inline
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.

Custom-built classes with dedicated _repr_*_ methods

In our first example, we illustrate how objects can expose directly to IPython special representations of themselves, by providing methods such as _repr_svg_, _repr_png_, _repr_latex_, etc. For a full list of the special _repr_*_ methods supported, see the code in IPython.core.displaypub.

As an illustration, we build a class that holds data generated by sampling a Gaussian distribution with given mean and variance. The class can display itself in a variety of ways: as a LaTeX expression or as an image in PNG or SVG format. Each frontend can then decide which representation it can handle. Further, we illustrate how to expose directly to the user the ability to directly access the various alternate representations (since by default displaying the object itself will only show one, and which is shown will depend on the required representations that even cache necessary data in cases where it may be expensive to compute.

The next cell defines the Gaussian class:

In [2]:
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_svg_())
    
    # 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)

Now, we create an instance of the Gaussian distribution, whose default representation will be its LaTeX form:

In [3]:
x = Gaussian()
x
Out[3]:

We can view the data in png or svg formats:

In [4]:
x.png
Out[4]:
In [5]:
x.svg
Out[5]:

Since IPython only displays by default as an Out[] cell the result of the last computation, we can use the display() function to show more than one representation in a single cell:

In [6]:
display(x.png)
display(x.svg)

Now let's create a new Gaussian with different parameters

In [7]:
x2 = Gaussian(0.5, 0.2, 2000)
x2
Out[7]:

We can easily compare them by displaying their histograms

In [8]:
display(x.hist)
display(x2.hist)

Adding IPython display support to existing objects

When you are directly writing your own classes, you can adapt them for display in IPython by following the above example. But in practice, we often need to work with existing code we can't modify.

We now illustrate how to add these kinds of extended display capabilities to existing objects. We will use the numpy polynomials and change their default representation to be a formatted LaTeX expression.

First, consider how a numpy polynomial object renders by default:

In [9]:
p = np.polynomial.Polynomial([1,2,3], [-10, 10])
p
Out[9]:
Polynomial([ 1.,  2.,  3.], [-10.,  10.], [-1.,  1.])

Next, we define a function that pretty-prints a polynomial as a LaTeX string:

In [10]:
def poly2latex(p):
    terms = ['%.2g' % p.coef[0]]
    if len(p) > 1:
        term = 'x'
        c = p.coef[1]
        if c!=1:
            term = ('%.2g ' % c) + term
        terms.append(term)
    if len(p) > 2:
        for i in range(2, len(p)):
            term = 'x^%d' % i
            c = p.coef[i]
            if c!=1:
                term = ('%.2g ' % c) + term
            terms.append(term)
    px = '$P(x)=%s$' % '+'.join(terms)
    dom = r', domain: $[%.2g,\ %.2g]$' % tuple(p.domain)
    return px+dom

This produces, on our polynomial p, the following:

In [11]:
poly2latex(p)
Out[11]:
'$P(x)=1+2 x+3 x^2$, domain: $[-10,\\ 10]$'
In [12]:
from IPython.display import Latex
Latex(poly2latex(p))
Out[12]:
$P(x)=1+2 x+3 x^2$, domain: $[-10,\ 10]$

But we can configure IPython to do this automatically for us as follows. We hook into the IPython display system and instruct it to use poly2latex for the latex mimetype, when encountering objects of the Polynomial type defined in the numpy.polynomial.polynomial module:

In [13]:
ip = get_ipython()
latex_formatter = ip.display_formatter.formatters['text/latex']
latex_formatter.for_type_by_name('numpy.polynomial.polynomial',
                                 'Polynomial', poly2latex)

For more examples on how to use the above system, and how to bundle similar print functions into a convenient IPython extension, see the IPython/extensions/sympyprinting.py file.
The machinery that defines the display system is in the display.py and displaypub.py files in IPython/core.

Once our special printer has been loaded, all polynomials will be represented by their mathematical form instead:

In [14]:
p
Out[14]:
$P(x)=1+2 x+3 x^2$, domain: $[-10,\ 10]$
In [15]:
p2 = np.polynomial.Polynomial([-20, 71, -15, 1])
p2
Out[15]:
$P(x)=-20+71 x+-15 x^2+x^3$, domain: $[-1,\ 1]$