Review: ipython notebooks

The development version of ipython recently added a "notebook" interface mode. This resembles the interface of MAPLE or Mathematica, in which you can intermingle blocks of formatted text, blocks of code and output, and plots. My feeling, after working with it for a bit, is that it is very well suited to the kind of work I do when (for example) writing blog posts demonstrating some algorithm. For developing actual reusable software, not so much.

In order to give it a proper test, I applied it to a problem that a friend asked me about. It's about the card game Set: how often do you run into a tableau with no sets? The game quotes a figure of one time in 25, but that's for tableaux dealt from scratch. As you play the game, you remove and replace sets, and it often seems as if no-set tableaux arise more often when the cards are not freshly-dealt. The game's rules are a lovely minimalist mathematical exercise, but once you start removing and replacing cards an analytical solution becomes impractical. Simulation to the rescue! And this seemed like a nice test problem for ipython.

You can see the results here (PDF). For my comments on ipython, read on.

Edited to add: notebooks have improved a lot since this review, or the later one.
A brief aside: I hate software reviews that focus on installing the software. I realize that as a reviewer this is what you spend a lot of your time doing, but an actual user can just install once and be done with it, particularly once it makes it into a stable distribution (or whatever the Mac and Windows equivalents are). That said, the version of ipython I used is straight from git, so one should expect a certain awkwardness.

Description

Starting up ipython's notebook interface is a little awkward: you run ipython notebook --pylab=inline in a terminal, and it prints out various starting messages then a URL looking like http://127.0.0.1:8888/. You then go to that URL in a web browser. What's happening is that ipython is running as an "engine" with a built-in web server, and your browser is providing all the UI. You're presented with an awkward file browser sort of thing that lets you create a new notebook or open an existing one.

ipython notebook files are saved with an extension of .ipynb, and internally they're what looks like python source code - lists of dictionaries of strings in a human-readable format. They contain the text, input, output, and plots (in what I think is base64-encoded PNG). So it wouldn't be too hard to recover the contents if ipython somehow went belly-up.

From a user's point of view, an ipython notebook is divided into "cells", each of which may contain either text or python input and output. There's a simple UI in a left pane that lets you switch modes, insert and delete blocks, et cetera. There is also some UI for controlling the engine, which I'll get to later.

Text cells, when you edit them, are simple plain text input forms (though, annoyingly, they don't word wrap). When focus leaves, they are rendered as markdown. If you haven't used it, markdown is one of the minimalist text formatting languages, in which simply writing text produces nice text, and markup is done using ascii characters in a way that's fairly natural if you're used to email: for example, words are emphasized using *stars*. But it's fairly flexible; apparently you can embed more-or-less arbitrary HTML and the right thing will happen. More interesting to me is that you can embed LaTeX math elements: either inline as \(x^2\) or "displayed" on its own line as $$\int e^{-ift} dt$$. The math is rendered using a magical little tool called mathjax, which is served up locally or falls back to a web-accessible one. This produces MathML in modern browsers but I assume falls back to good old embedded PNGs on older ones (and hopefully falls back even further to raw LaTeX on text-mode browsers; I haven't tested it). In short, you can put math in the text, and it Just Works.

Python cells can contain multiple lines, though you must return to zero indentation at the end of each - that is, you can't break up a function or class definition into multiple cells. But you can have blocks of statements. When you hit shift-enter, the block is sent to the kernel and executed, and the block is stored as In[37] (say). So the array In, which is visible from python, records all your input in the order the engine received it - which is not necessarily the order the cells appear in the notebook. You can (and I often did) edit and rerun cells; the displayed number is updated, and the kernel responds. This is nice in that it saves you having to rerun all your code just because you made a typo in the final plotting code.

When you run a python block, it is sent to the engine. But since you're working within a web browser, you can keep on editing and even sending blocks to the engine (they get added to the queue). When a block finishes, its output is displayed. More specifically, the output of the last top-level statement/expression is displayed, as Out[37] (say), unless it is None (conveniently, adding a semicolon at the end makes this happen). Any printing you do also winds up in the python block, between your input and Out.

Most interestingly, if you are using matplotlib (and if you turned on the inline option when running the engine, which I did), if you plotted something, the plot appears in the python block. You can't interact with it the way you can with matplotlib pop-up windows, and it is by default a fairly low-resolution PNG, but it's extremely convenient. I think you can also generate a PDF using savefig in the usual way, but that's not what appears in the output block. In any case, this embedding of plots right into the notebook is really handy; they feel like any other output from the python code.

Python in general and especially numpy are fairly well documented. Traditionally one of the nice features of ipython has been that it made it really convenient to access this documentation from the python shell. The notebook interface is not bad in this respect, though a little clumsy. If you run (say) "np.histogram?" in a python block, a pane pops up containing the help for this function. On my screen, at least, it was generally too small to see the parts of the help I cared about without scrolling (i.e. switching from keyboard to mouse) and it also tended to obscure the python code I was actually writing. I also usually had to create a new block to run it in (requiring another switch from keyboard to mouse, then back to keyboard to ask for the help, then back to mouse to read the help). It's not obvious how to make the help window go away, but if you click on the divider it'll disappear (actually minimize, I think). You then, if you want your notebook to stay tidy, have to delete the place you asked for help. It's kind of a pain. It also happened to me once that when I was trying to query a python built-in function ipython instead showed me the numpy function of the same name, even though it was the python function that was referred to in that scope. In the end, I often found myself using the web-based documentation, for which there are buttons conveniently provided in the notebook control pane.

Tab completion has traditionally been another of the selling points of ipython, and it is provided, sort of. When you hit tab while editing a python block, it pops up a list of possible completions in a little window with a scroll bar. As far as I could tell, you have to switch to the mouse to make this go away. Since I generally just use tab completion to save typing, this was not very helpful.

The editing UI quirks are kind of annoying, but I found interacting with the engine one of the strong points of ipython's notebook interface. Most of the time, it's enough to just hit shift-enter on blocks you want to run. You can select several and run them all, but I didn't usually bother with this, since shift-enter advances you a block, so I could just keep hitting shift-enter until I got to the right point (and you don't need to wait for them to finish!). On the other hand, I did use the "restart" button to wipe the engine clean and then the "run all" button to get a record of a fresh run. Since when you load a notebook, nothing is executed, but all the old output is still visible, this is a nice final step before saving the output. It also happened a few times that the engine somehow died, and after a little while ipython sensed this and asked if I wanted to start a new one. You can send a virtual control-C, but I don't see a way to forcibly kill the engine if you need to (for example it's deep in some horrible Fortran code and won't respond to control-C). This never happened to me, so maybe ipython does the Right Thing.

Saving notebooks is reasonably well supported. There's a button to save it as a .ipynb file; I don't think it autosaves, but I'm not sure. You can also download it to a web browser as either .ipynb or as straight python (which contains all the markdown in comments, but none of the output). Most usefully, you can hit "print" and get a new browser window with a non-interactive HTML version. This can be either printed from your browser or saved. Unfortunately I don't much like how firefox prints HTML (in particular if you have very long lines in your python code it will shrink the entire document until they fit), but it does produce a reasonable PDF. The HTML output is also fine, but of course suffers from the nuisance of needing various supplementary files, in particular images. It's also a self-contained HTML file, which is hard to embed in (say) a blog post.

Opinion

So: that's what the notebook interface does. What did I think of it?

I feel uncomfortable using the buzzwordy "workflow", but really, the ipython notebook interface makes for a lovely workflow for the sort of using-python-to-figure-out-an-algorithm that I often do. I just start banging out the python code, breaking it up into natural blocks and executing them as I go. When something goes wrong, I can just fix and rerun that particular block. If it affects later ones, or for example if I changed a parameter, I can just shift-enter through them all. I can write explanations or comments in a nice-looking way that allows math. When I'm figuring things out, I can use markdown/math blocks as a sort of virtual chalkboard, with python blocks to test the ideas and inline plots to check the results. When I'm done, I can prune out all the failed ideas and add clearer explanations.

There are annoyances to the interface too:
  • I find accessing help fairly awkward, not just because the built-in help requires a lot of keyboard-to-mouse trips but because the notebook interface takes up enough room on my screen that it's not easy to have another browser window with a help site open alongside.
  • I find it unclear how to control the sizing of the inline plots; set_size_inches does work, but the size in inches is not the actual on-screen size in inches (perhaps this is a DPI issue?).
  • The format of the inline plots is something of a problem too: the screen-resolution PNGs are clearly the right solution when working on the document or when saving it for HTML display on the web. But for printing they're kind of low-resolution (and raster rather than vector). It would be nice if they could be made clickable, yielding a higher-resolution or SVG (or PDF) version. While a pure-SVG option is available, it's not clear how to turn it on, and in any case I'm hesitant to do so because I sometimes plot tens of thousands or millions of points (to get a smooth curve, for example, or because I just plain have that many and it'd be a pain to weed them) and I'm afraid of producing a monstrous SVG that will make my browser melt down. On the other hand, plotting can sometimes be slow, and if the code had to render a second, print-quality, version every time, that could be annoying during the testing-out phase. Perhaps allowing the output format (screen PNG, PNG plus clickable SVG, SVG) to be conveniently selected on an image-by-image basis?
  • I miss version control. I don't know how one could integrate it into this kind of setup, but it sure is nice to be able to go back to old versions. Maybe just having an option to keep emacs-style .ipynb~37~ backups would help?
  • Autosave - it'd be nice to have a vi-style autosave/recovery file plus a visual indicator of its up-to-dateness (unless it's always up to date).
  • Publishing the results is a pain. My solution at the moment is to host the files on my work web server, which means they're likely to disappear long before this post does. I'd love to be able to just include the output in a blog post and have it work, but I don't see how. I suppose in an ideal world I'd just copy the entire thing from the "print" window and paste it into the blog's "compose" box, and it would work, images and math and all, but I think that's a little unrealistic. Maybe a sort of HTML-fragment output mode?
It should be added that this is an experimental version pulled from git, so this should be viewed more as a wishlist for the developers than a list of problems you should expect from a stable release.

Overall I have to say that the ipython notebook interface is very nice. I say this as someone who worked on MAPLE, back in the day, and avoided its notebook interface in favour of the console mode, ascii-art math and all. ipython notebooks encourage a very comfortable literate-programming style. I don't think it's suitable for writing reusable python software, though it would be a nice way to write a tutorial or demonstration. I plan to use it for trying out interesting algorithm ideas, for work or for fun.

14 comments:

Min RK said...

G+ comment reposted here, per request:

Thanks for playing with it, and the thorough feedback! I'll post some responses to a few of your notes, in order (mostly):

* awkward launch: Since launching the notebook server is a one-line Python call, making a double-clickable Launcher/App/exe is very simple, and we should probably have some examples of such things in the docs.

* The ipynb is just JSON, which does look like a lot a Python dict, but is very easy to read in just about any language.

* soft-wrap is a good idea, and probably (hopefully) just a small codemirror setting. Thanks! The output is properly wrapped in my experience, I hope this is yours as well.

* nitpick - you don't have to end cells with zero-indent (unlike Terminal IPython), as cells can end on a content line, but of course you are right that it must functionally be a fully contained scope, and you can't span a scope across cells.

* interacting with figures - we want this. I think the Sage folks really want it. It will happen eventually, but will require some serious JavaScript.

* The image format is PNG by default, because it is rendered more reliably than SVG. But if your goal is PDF output, SVG will certainly perform better. To do this, simply:

from IPython.zmq.pylab import backend_inline
cfg = backend_inline.InlineBackendConfig.instance()
cfg.figure_format = 'svg' # 'png' to switch back

If you want this to be the default in the notebook, add the line:

c.InlineBackendConfig.figure_format = 'svg'

to your ipython_notebook_config.py (or just ipython_config.py if you want it to be the default everywhere the inline backend is used, rather than just in the notebook).

* Our display system already does support multiple simultaneous mimetypes, so it shouldn't be much work to get the multiple PDF/SVG/PNG export behavior you described. That would definitely be cool!

* PNG/SVG figure export currently hardcodes 72 dpi resolution, so you can set the figure size with `matplotlib.rcParams['figure.figsize'] = (x,y)`. If you are running in pylab mode, this is exposed in the user_ns as just `figsize(x,y)`.

It is likely that we will want to allow this DPI to be configurable, and maybe we should just respect the matplotlib 'figure.dpi' param, though the 72dpi setting makes the most sense in the qtconsole, which has fixed display dpi.

* The pager for help-output should definitely be keyboard-dismissible. It should also probably either allow dragging the pane-splitter, or at least include full-pane view, so you can read it better.

Min RK said...

...continued...

* I don't find tab-completion to be mouse-dependent. You can select with up/down arrows, and choose with Enter. If you just continue typing after tab, the pop-up is dismissed. The popup is also dismissed with a second TAB. If this is not what you see, can I ask your browser and platform?

* it does not autosave. I think autosave is a good idea, but Fernando hates autosave.

* We spent a confusing couple of days looking at the print-output. It does weird things on various browsers for large cells, etc., so we definitely need to work on that part. Exporting individual cells with their output would probably be useful for embedding, wouldn't it?

* We absolutely intend to have first-class version control. I think the intended model is something like using git for an OSX 10.7-style Versions, so there would be a Save (`git commit --amend`), but also Save Version, which makes a new commit. Once we have integration with git, then perhaps autosave will be less objectionable to those who don't like it, as they will have total control preventing autosave from clobbering work.

* Stéfan van der Walt has been looking at gist integration, for easy publication/versioning/sharing of code. The nice thing about gist is it doesn't even rely on a local install of git, and can be done entirely with some simple urllib calls.

* We have a lot of work to do on publishing output, but this is also a top priority. The more publication use cases we get described to us, the better we will be able to figure out what should be done.

The more you play with it and have ideas, the better. Keep the feedback coming!

Fernando said...

Argh, I hate blogger! I'd written up a long reply and blogger threw an authentication error on post, and simply destroyed it... Anyway, I'll try to now summarize the main points...

- First, big thanks for your review and feedback! This is super-early alpha code, so rough patches are to be expected, but your input helps us know what to prioritize.

- Some of the quirks (like improper word wrapping) come from the code editing widget we're using, CodeMirror. We hope it will continue improving... Part of the issue is also that we use it in an unusual way, with a ton of instances embedded (one per cell) which is very different from the typical pattern of editing one single file per session it was designed for. So it may not be completely their fault. But still, we'll work with the CodeMirror devs to improve the experience.

- SVG/PNG: do you think having a magic to control these parameters easily at runtime would be good? It's not particularly hard to do.

- autosave: if we implement it with an _autosave filename that's *different* from the default filename the user is working on, I have nothing against it. I just don't want autosave overwriting my working file automatically without my knowledge, as I might be in the middle of something I want to back out from simply by closing the file.

- better publication support: this is perhaps our most important use that we know is limited right now, but there's a fair amount of work to do there. We ultimately want good integration with shpinx to support a usage where you go from interactive exploration to final latex document in one shot (perhaps not journal publishing, but at least sphinx-based pdf/html generation). Help most welcome :)

- better web start: the code is trivial, we just haven't added it:

import webbrowser
webbrowser.open('http://localhost:8888')

that is supposed to open your default browser in a cross-platform way. We simply need to add those two lines after the print statement.

- writing 'permanent' code: since you can export a working .py file, it's not unreasonable to think of having a full module that you develop in the notebook intearactively, but use from other projects in its .py form. But having said that, this is not meant to replace classic edit-in-emacs/vim workflows, just to complement them with better tools for interactive exploration than what 'classic' ipython (or even the new qtconsole) provide.

- kernel control: the 'restart' button will kill and restart a kernel no matter what it's doing, even if it's in the middle of a deep fortran loop. 'interrupt' sends it a Ctrl-C (SIGINT), which it may ignore in some circumstances. We need to add keyboard shortcuts for these.

- awkward control of the help viewer: we know, it drives us crazy too :) It's just that we haven't been able to get a cleaner implementation thanks to some limitations of JQuery/Javascript (and perhaps our lack of expertise with JS). This is one area where the help of a Javascript guru would be very welcome.

- git/gist: as Min said, super-high on our list, will get done (at least the gist part should be done very soon).



Keep the critical feedback coming, we'll definitely listen!

Cheers,

f

Anne M. Archibald said...

Interacting with figures: I think the way to go would be to use SVG - it is intended, after all, to be a replacement for Flash. So suitable javascript in the matplotlib output should allow the SVG output to be interactive.

Anne M. Archibald said...

Tab completion: the way I use tab completion in (e.g.) the shell is to type a few characters, hit Tab so that it fills in everything up to the first point of ambiguity (if any). Then I type another letter or two and hit Tab again until I get the right value. In particular, to do this I don't need to look at the screen, if I know what the possibilities are.

Anne M. Archibald said...

There's an intermediate between autosave and no autosave - autosave but to a temporary file that is deleted upon saving. Version control would basically solve this, but finding a version control setup that will make everyone happy is likely to be a challenge.

Fernando said...

Anne, I just posted a long comment a few hours ago, did it get lost? I tried to address a number of your points...

Anne M. Archibald said...

SVG/PNG: It might be enough to simply use SVG by default but make it easy to request that a particular image be PNG (say because I know it'll have a zillion points). Or use PNG throughout but make it easy to switch to SVG so I can rerun the script before hitting "print". In any case it might be handy to have UI to set settings like this.

Anne M. Archibald said...

Publication: I'm not so interested in journal-quality output; that will always need manual fiddling. My particular use-case is that I'd like to be able to post a notebook on my blog. I'm not sure what the right answer is; blogger will accept restricted HTML, but I'm not sure how to handle images.

Fernando said...

- images: it sounds like it would be a good idea to have a magic to make it easy to toggle this at runtime. You could then set your default to be svg and can toggle to png as you need it.

- publication: our most immediate target will be good sphinx support. That lets us leverage the sphinx toolchain, and it could be possible to write support for creating blogger-happy html from sphinx... Else, we'll all need to move from blogger to another blog tool that makes it easier to have math, code and figures :) (I have to say that I find the blogger tools a bit primitive in that regard).

Anonymous said...

I have a large 24'' screen that I'm using by switching X in 129 DPI mode (I calculated it myself to fit the DPI to the desired physical size of the letters).

So I wouldn't expect anyone having the same DPI, especially that large and ultrasharp screens are becoming more and more common...

erhan said...

Min: you wrote:

* awkward launch: Since launching the notebook server is a one-line Python call, making a double-clickable Launcher/App/exe is very simple, and we should probably have some examples of such things in the docs.

Do you have an example of this you can share with us?
Thanks...

erhan said...

OK, I figured out a way that works in Mac OSX finder:

Make the following applescript be the default "application" (stored in /Applications under a suitable name such as openIPynb) to open your *.ipynb files (upon double clicking on them):
**********************************
on open inputfile
tell application "Finder"
activate
set theFolder to (folder of the front window) as text
set theFolder to POSIX path of theFolder
tell application "Terminal"
activate
do script "ipython notebook pylab=inline --NotebookManager.notebook_dir=" & theFolder
end tell
end tell
end open
*****
The dashboard in ipython notebook will then list all the *.ipynb files found within the folder, generating independent python kernels for different folders.

erhan said...

OK, I figured out a way that works in Mac OSX finder:

Make the following applescript be the default "application" (stored in /Applications under a suitable name such as openIPynb) to open your *.ipynb files (upon double clicking on them):
**********************************
on open inputfile
tell application "Finder"
activate
set theFolder to (folder of the front window) as text
set theFolder to POSIX path of theFolder
tell application "Terminal"
activate
do script "ipython notebook pylab=inline --NotebookManager.notebook_dir=" & theFolder
end tell
end tell
end open
*****
The dashboard in ipython notebook will then list all the *.ipynb files found within the folder, generating independent python kernels for different folders.

Share it