Python REPL as a presentation framework
I needed a presentation software for a talk at a Python meetup (in Karlsruhe, but I’ve since held the talk at the Python meetup Stuttgart, too). I wanted something simple with which I could show code easily, but ideally there would be some element of interactivity, too (what if I could demonstrate the code I show within the presentation itself?).
One solution would of course be to fake it: I could just pretend something is
running and show the output I would see, maybe after some delay. This is
something even PowerPoint can do. But this is no fun, I wanted real Python
execution in the background. Next idea: Build my own script that can somehow
take Python code and eval/exec it inbetween showing slides. Maybe something
browser-based? Or I could make a fake Python REPL and interpret some special
strings as commands for showing “slides” in a terminal.
Then I saw a talk by the mad scientist of the Python universe where he appeared to kind of do what I wanted, with what he called a “modded Python interpreter”. That gave me an idea: What if I didn’t have to write a fake REPL, but could use the real one?
Objects in Python can determine how their representation looks like, so I could
define a custom object that remembers the slide number and prints different
text each time it is repr()d. If I want to have a “next” and “previous”
operation, I could have two special objects that share the state of which slide
I’m currently on.
__repr__ methods with side-effects was the first idea, but then I found
another, easier, hackier solution: sys.displayhook. It’s a function that gets
called by the Python REPL whenever an object is being printed interactively,
its job is this printing. If I changed that, I wouldn’t even need special
objects (and in the final version I don’t; the two variables n and p are
just assigned to the strings "n" and "p" and my custom displayhook does the
rest).
Next challenge: I wanted to do what’s known as a “prover” in the world of
magicians: Some way of (falsely) convincing the audience that there is no
trickery going on. Ideally, I would want to start a Python interpreter with the
python3 command and have the presentation framework function without having
to import things or running any extra code. While I could have used a shell
alias or something like that to do this, that would have meant I would have had
to recreate the Python REPL header, since python -i some_module.py doesn’t
print it. Instead, I went for a different tool: Environment variables. Python
looks at a few at startup, one of them being PYTHONSTARTUP, which I just had
to set to my script that replaces the displayhook and I’m done. I can do that
before the presentation or even put it in my .zshrc, so nobody needs to know.
I had most of the program set up, but I was still missing the actual presentation (who hasn’t written a presentation framework in order to procrastinate preparing a talk?). I wanted it to be easily editable, and I wanted the ability to show code and have it executed in the background.
As usual, I chose Markdown, as it’s easy to edit, expressive enough to make
simple presentations, and text-based. A combination of mistune and colorful
allowed me to write a custom Markdown renderer that outputs ANSI escape
sequences to do color and font weight. Using pygments I could
syntax-highlight code. Markdown supports specifying a language in the beginning
of a fenced code block, so I invented some fake languages like exec, and
exechidden that would not only highlight like Python, but actually execute
the code and allow the resulting names to be used:
def block_code(self, text, lang=None):
    if lang and lang.startswith("exec"):
        bindings = {}
        exec(text, bindings)
        for key, value in bindings.items():
            if key == "__builtins__":
                continue
            setattr(builtins, key, value)
The only Markdown feature I found hard to do in a terminal were headings. Yes, I could have just printed them as in Markdown, with an octothorpe in front, or made them all-caps. I wanted different font sizes.
Enter Pillow, your friendly neighbourhood PIL clone. Whenever I encounter a
heading, I draw text on a PIL.Image that has the size of my terminal window.
I then convert the 256-color-grayscale image to an 8-color-grayscale image and
then use the shaded block unicode characters (░▒▓█) to print the result.
Finally, I wanted multi-language support. I prepared the talk in English, but I
wanted to give the audience an option to switch to German instead. To do this
in an interesting way, I reused the sys.displayhook trick from earlier and
introduced some additional piece of state: the mode. Usually, the mode is set
to "normal", but when I type l, which contains "l", the shortcut for
language selection, it switches to a special mode that will cause the display
hook to intercept the next input and set a configuration value based on a
selection from a menu:
>>> l
[0] german
[1] english
[2] romanian
>>> 2
Switching language to romanian
>>>
You can find the whole thing at https://github.com/L3viathan/triotalk, including the actual talk.
But there’s one more thing. If someone had figured this whole thing out during the talk and called me out on it, I wanted to have an extra ace up my sleeve.
I found the answer in sys.excepthook, which is similar to
sys.displayhook, but is responsible for printing exceptions and their
stacktraces. This allowed me to do this (17 is not really the return value,
it is merely printed):
>>> 4/0
17
