Python, interactively

I recently realized, after including code from interactive Python sessions in recent posts (this one and this one), that I have more to say about my use of the Jupyter console than I covered in this post back in December.

First, although the Jupyter console can run interactive sessions in Julia and R, I use it only for Python, which means I’m really working in IPython, which is what the Jupyter console runs by default. Here’s the startup message that appears after I run jupyter console in the Terminal:1

Jupyter console 6.6.3

Python 3.13.5 (main, Jun 11 2025, 15:36:57) [Clang 17.0.0 (clang-1700.0.13.3)]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.3.0 -- An enhanced Interactive Python. Type '?' for help.
Tip: `?` alone on a line will brings up IPython's help

It displays the version numbers for Jupyter console, Python, and IPython. Everything after the Jupyter console version number is the IPython “banner.” The tip at the end varies from session to session.

So if all I’m really doing is running IPython, why not do so directly? It’s a good question, and one I don’t have an especially satisfactory answer to. I started using Jupyter with notebooks. When I decided I preferred working in a console session instead, I just stayed in the Jupyter environment out of habit. At the time, I probably thought it was different from IPython and possibly an improvement. Maybe confessing publicly to this lame reason will get me to shift to IPython directly. To start the ball rolling in that direction, I’ll just refer to IPython from now on.

If you look back at the startup message, you’ll see that I’m running Python 3.13. You may have heard that this version has improvements to its standard interactive environment. It does, but it still isn’t as nice as working in IPython, mainly because of the magic commands.

When I’m working on a Python script, I try to test all the code as I write it, Typically, this means writing the code in BBEdit, saving it to a file, and testing it in IPython. The magic commands %run and %reset are the keys to this workflow. Typing

%run script.py

in an IPython session runs all the code in the file and keeps the session active. I can then check on what’s been written so far: calling functions with a variety of inputs, inspecting the values of variables, making sure loops and conditionals work the way they’re supposed to.

After adding to or fixing the code, I return to the IPython session and type

%reset -f

This clears out all the names defined earlier in the session so the next %run starts fresh.

Because IPython keeps a history, I seldom have to type out %run script.py or %reset -f in full. Typing ⌃R puts IPython in an interactive backward search mode. Typing %ru after that brings up the most recent %run command; typing %re brings up the most recent %reset.

Even better—and this is a Bash tip, not an IPython tip—because I have these commands in my .bashrc file,

bind '"\e[A":history-search-backward'
bind '"\e[B":history-search-forward'

I can type %ru and then the ↑ to bring up the most recent %run command. Similarly with %re and ↑. I learned these bindings from a long-ago post by Craig Hockenberry and they work in all Terminal sessions, not just IPython.

(Depending on how you have things configured, IPython may show you the most recent matching command as soon as you start typing. These are called autosuggestions. I find the feature very annoying and don’t use it.)

The %rerun command can be useful when you have a handful of commands that you want to run again in the same order. For example,

%rerun -l 4

will rerun the last four commands. I confess I don’t use %rerun as much as I should. I tend to use ↑ to go back through the history again and again, only realizing after I’m done that I could’ve saved time using %rerun.

After %run and %reset, my most commonly used magic command is probably %history. Using it like this,

%history -opf session.txt

creates a file with all the input and output of the current session. The -o option tells it to include the output; the -p option tells it to put >>> in front of all the input lines, making it look like a regular Python interactive session (even though it wasn’t); and the -f <filename> option tells it where to save the output. You can also give it a range of session lines to print. This is what I did to create the interactive Python code I added to those recent posts.

Another %history option, helpful when you’re trying out code in an IPython session and finally hit upon what you want to add to your script, is -t. Using this without -p or -o will print out the session code in a format that can be pasted directly into your script.

Finally, IPython has several ways to incorporate previous output into a command. Suppose our session has gone like this:

In [1]: a = 1; b = 2; c = 3

In [2]: b*c
Out[2]: 6

In [3]: a + b*c
Out[3]: 7

In [4]: a + c**2
Out[4]: 10

At this point, we can multiply the third Out by c like this:

In [5]: Out[3]*c
Out[5]: 21

In and Out are not just labels, they’re Python variables that are continually updated during the session, and you can refer to them at any time. If typing Out and brackets is too much work (it is for me), you can use an underscore and a number to refer to previous output, e.g.,

In [6]: _4**c
Out[6]: 1000

This simplified way of referring to previous output is reminiscent of Python’s long tradition of using the underscore (all by itself) to refer to the most recent output in an interactive session. IPython follows that tradition and extends it: two underscores (__) refers to the second most recent output, and three underscores (___) refers to the third most recent output. But that’s where it stops—apparently the IPython developers consider four underscores beyond the pale.

I’m sure there are other things I do in interactive Python sessions, but these are the ones I use often enough to remember.


  1. You may recall from the December post that I have jupyter console aliased to jc in my .bashrc file, so I haven’t typed out jupyter console in full in several years.