3 min read

Programmatically Creating Styled Cells In Jupyter Notebooks

Programmatically Creating Styled Cells In Jupyter Notebooks
Why is this so annoying to do???

I've been playing around with the idea of using Jupyter notebooks as a narrative game interface for a while, and finally got around to doing some prototyping this week. One thing I really wanted to do was require users to run code cells in Jupyter to unlock story beats: Jupyter notebooks are usually presented as a step-by-step tutorial, which is perfectly fine if you're trying to teach someone how to run a linear regression in sklearn but less so if you're trying to build suspense over the course of a story.

It's pretty easy to get output from just running a Python function, obviously:

But I wanted to be able to do two things: programmatically create code cells, and programmatically create styled text cells.

Both of these are relatively easy to do if you don't get caught in the weeds like I did (we'll get to that):

You can use get_ipython().set_next_input("your code here") to programmatically create a cell that the reader of your notebook then has to run. Easy enough to build on top of this by obfuscating some code that outputs your story beats in a separate file.

Unfortunately, set_next_input can't set a cell type other than code (and is limited to one cell at a time, as I understand it). If you want to overcomplicate things, you can also use the JupyterFrontend app:commands API to inject code as well and it can get more granular than set_next_input (e.g. you can run it in a loop to create N cells), but it still couldn't set a cell type or render styled text:

(It's possible that if I understood the notebook JSON structure better I might be able to use it for other purposes but I couldn't find any helpful documentation)

I kept digging until a JupyterLab forum thread gave me the answer I didn't want:

:(

There are plenty of ways to create styled output from a code cell, but I got sucked into a bit of a rabbit hole: I wanted to create a styled input cell programmatically. I am not sure exactly why I wanted to do this other than it was annoying to me that I could only create a code cell with IPython's built-in methods.

I'll include those styled output methods here just in case they're useful to someone:

run_cell_magic and display_markdown both work perfectly fine for displaying formatted text, but again, they don't display as inputs (you can tell by the lack of a little number next to them that indicates they were run as code).

At this point I had dug into StackOverflow, the official Jupyter forum, GitHub issues, and the API documentation for IPython, which is about where I tap out when doing research. This seemed like a problem somebody should have solved, but my Google-fu may have been lacking. I finally stumbled across the answer I was looking for when I found an issue recommending someone make a widget for this problem - it turned out there was already one that met my needs!

I finally found my answer after a lot of digging in the iPyWidgets documentation: a simple HTML widget.

I could go even further down the rabbit hole here and get upset that it's not numbered as its own execution block, rather sharing the parent function's index (14), but I think that's a reasonable stopping point for the day. Next up: animating Jupyter cells!

Sources/The Weird Search Path I followed:

Stackoverflow answer 1

Magic commands for iPython

Stackoverflow answer 2

Discourse thread

Github thread that led me to check iPyWidgets

iPyWidgets string documentation