Adding keyboard shortcuts to the Python REPL
I talked about the new Python 3.13 REPL a few months ago and after 3.13 was released.
I think it’s awesome.
I’d like to share a secret feature within the Python 3.13 REPL which I’ve been finding useful recently: adding custom keyboard shortcuts.
This feature involves a PYTHONSTARTUP
file, use of an unsupported Python module, and dynamically evaluating code.
In short, we may be getting ourselves into trouble.
But the result is very neat!
Thanks to Łukasz Llanga for inspiring this post via his excellent EuroPython keynote talk.
The goal: keyboard shortcuts in the REPL
First, I’d like to explain the end result.
Let’s say I’m in the Python REPL on my machine and I’ve typed numbers =
:
I can now hit Ctrl-N
to enter a list of numbers I often use while teaching (Lucas numbers):
1
|
|
That saved me some typing!
Getting a prototype working
First, let’s try out an example command.
Copy-paste this into your Python 3.13 REPL:
1 2 3 4 5 6 7 8 9 10 11 |
|
Now hit Ctrl-N
.
If all worked as planned, you should see that list of numbers entered into the REPL.
Cool!
Now let’s generalize this trick and make Python run our code whenever it starts.
But first… a disclaimer.
Here be dragons 🐉
Notice that _
prefix in the _pyrepl
module that we’re importing from?
That means this module is officially unsupported.
The _pyrepl
module is an implementation detail and its implementation may change at any time in future Python versions.
In other words: _pyrepl
is designed to be used by Python’s standard library modules and not anyone else.
That means that we should assume this code will break in a future Python version.
Will that stop us from playing with this module for the fun of it?
It won’t.
Creating a PYTHONSTARTUP
file
So we’ve made one custom key combination for ourselves.
How can we setup this command automatically whenever the Python REPL starts?
We need a PYTHONSTARTUP
file.
When Python launches, if it sees a PYTHONSTARTUP
environment variable it will treat that environment variable as a Python file to run on startup.
I’ve made a /home/trey/.python_startup.py
file and I’ve set this environment variable in my shell’s configuration file (~/.zshrc
):
1
|
|
To start, we could put our single custom command in this file:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Note that I’ve stuck our code in a try
–except
block.
Our code only runs if those _pyrepl
imports succeed.
Note that this might still raise an exception when Python starts if the reader object’s command
attribute or bind
method change in a way that breaks our code.
Personally, I’d like to see those breaking changes occur print out a traceback the next time I upgrade Python.
So I’m going to leave those last few lines without their own catch-all exception handler.
Generalizing the code
Here’s a PYTHONSTARTUP
file with a more generalized solution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
This version uses a dictionary to map keyboard shortcuts to the text they should insert.
Note that we’re repeatedly building up a string of Command
subclasses for each shortcut, using exec
to execute the code for that custom Command
subclass, and then binding the keyboard shortcut to that new command class.
At the end we then delete all the variables we’ve made so our REPL will start the clean global environment we normally expect it to have:
1 2 3 4 |
|
Is this messy?
Yes.
Is that a needless use of a dictionary that could have been a list of 2-item tuples instead?
Yes.
Does this work?
Yes.
Doing more interesting and risky stuff
Note that there are many keyboard shortcuts that may cause weird behaviors if you bind them.
For example, if you bind Ctrl-i
, your binding may trigger every time you try to indent.
And if you try to bind Ctrl-m
, your binding may be ignored because this is equivalent to hitting the Enter
key.
So be sure to test your REPL carefully after each new binding you try to invent.
If you want to do something more interesting, you could poke around in the _pyrepl
package to see what existing code you can use/abuse.
For example, here’s a very hacky way of making a binding to Ctrl-x
followed by Ctrl-r
to make this import subprocess
, type in a subprocess.run
line, and move your cursor between the empty string within the run
call:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
What keyboard shortcuts are available?
As you play with customizing keyboard shortcuts, you’ll likely notice that many key combinations result in strange and undesirable behavior when overridden.
For example, overriding Ctrl-J
will also override the Enter
key… at least it does in my terminal.
I’ll list the key combinations that seem unproblematic on my setup with Gnome Terminal in Ubuntu Linux.
Here are Control
key shortcuts that seem to be complete unused in the Python REPL:
Ctrl-N
Ctrl-O
Ctrl-P
Ctrl-Q
Ctrl-S
Ctrl-V
Note that overriding Ctrl-H
is often an alternative to the backspace key
Here are Alt
/Meta
key shortcuts that appear unused on my machine:
Alt-A
Alt-E
Alt-G
Alt-H
Alt-I
Alt-J
Alt-K
Alt-M
Alt-N
Alt-O
Alt-P
Alt-Q
Alt-S
Alt-V
Alt-W
Alt-X
Alt-Z
You can add an Alt
shortcut by using \M
(for “meta”).
So r"\M-a"
would capture Alt-A
just as r"\C-a"
would capture Ctrl-A
.
Here are keyboard shortcuts that can be customized but you might want to consider whether the current default behavior is worth losing:
Alt-B
: backward word (same asCtrl-Left
)Alt-C
: capitalize word (does nothing on my machine…)Alt-D
: kill word (delete to end of word)Alt-F
: forward word (same asCtrl-Right
)Alt-L
: downcase word (does nothing on my machine…)Alt-U
: upcase word (does nothing on my machine…)Alt-Y
: yank popCtrl-A
: beginning of line (like theHome
key)Ctrl-B
: left (like theLeft
key)Ctrl-E
: end of line (like theEnd
key)Ctrl-F
: right (like theRight
key)Ctrl-G
: cancelCtrl-H
: backspace (same as theBackspace
key)Ctrl-K
: kill line (delete to end of line)Ctrl-T
: transpose charactersCtrl-U
: line discard (delete to beginning of line)Ctrl-W
: word discard (delete to beginning of word)Ctrl-Y
: yankAlt-R
: restore history (within history mode)
What fun have you found in _pyrepl
?
Find something fun while playing with the _pyrepl
package’s inner-workings?
I’d love to hear about it!
Comment below to share what you found.