bash_kernel gives Jupyter notebooks a real interactive bash
A bash kernel for IPython
At a glance
- What is it?
- takluyver/bash_kernel is a Python package that registers a Bash kernel for Jupyter, so a notebook cell talks to a genuine interactive shell with readline, and can render images, HTML and JavaScript back into the notebook. Installing it takes two steps, and on a PEP 668 system it takes five.
- Who is it for?
- bash_kernel fits a data scientist or engineer who wants real bash in a notebook cell, with readline history and rich output instead of a captured stdout log, and it fits pipelines that already depend on the Jupyter stack. It does not fit a machine whose bash package has been stripped of readline, and it does not fit a Python environment managed under PEP 668 unless you accept the pipx detour the project documents.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 43 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A notebook cell that talks to a real interactive bash
Most ways of running bash from a notebook capture output and give you none of the shell. bash_kernel takes a different route: it is a Jupyter kernel whose language is bash, and the kernel drives an actual interactive bash process rather than spawning a script per cell. The project points at two references for the mechanism, the Jupyter documentation on wrapper kernels and the Pexpect documentation on its replwrap module, which is where the terminal emulation comes from.
That design has visible consequences. History works, because there is a shell behind the cell rather than a fresh process. Tab completion and readline editing work, provided the shell on the machine supports them. And the shell is a persistent one, so the environment you set up in a cell is the environment the next cell sees.
The package itself is small and has only three runtime dependencies: `pexpect` at version 4.0 or newer, `ipykernel`, and `filetype`, which is what lets the kernel recognise content types. The build uses the flit backend with `flit_core` between 3.2 and 5, and the project is authored by Thomas Kluyver. The licence is BSD-3-Clause, which is permissive and comes with the usual requirement that the copyright notice and licence text travel with redistributed copies.
The repository is laid out with the package under `bash_kernel/`, a `binder/` directory for the hosted demo environment, a `CHANGELOG.md`, and the packaging metadata in `pyproject.toml`.
Two install steps, because the kernel has to be registered
Installing is a pip install followed by a separate registration step. The project states that it requires IPython 3, which is a floor that dates from the notebook era:
pip install bash_kernel
python -m bash_kernel.installThe second command is the one people miss. The first puts the package in the environment, the second tells Jupyter that a Bash kernel exists, which is why the two are shown together and why running the second command outside the right environment silently does nothing.
After that you start Jupyter and pick the kernel. The documented ways to run it are:
jupyter notebook
# In the notebook interface, select Bash from the 'New' menu
jupyter qtconsole --kernel bash
jupyter console --kernel bashIn the notebook interface the kernel is chosen from the New menu, and in the terminal consoles the kernel is named on the command line with the `--kernel bash` flag. The project also notes the alternative path for people who hit a broken installation, which is to debug the production build in a separate tool, though that is documented in the repository rather than in this package's own documentation.
Everything else lives on the Jupyter documentation site for the package, since the repository itself is a README plus source and tests.
The pipx and PEP 668 detour is the real installation problem
The package documentation is unusually candid about installation friction on modern Linux distributions. A recent-ish PEP 668 recommends installing Python applications with `pipx` rather than global installs with `pip`, and this is strongly suggested or enforced in current distributions. Since bash_kernel needs an extra step after the install to actually work, that policy turns a two-line installation into a five-line one.
The documented route is to install the Jupyter ecosystem with pipx and then inject bash_kernel into the same pipx virtual environment:
pipx install --include-deps jupyter
pipx inject --include-apps --include-deps jupyter bash_kernelThe injection step has to add the application entry points as well as the dependencies, because the console scripts Jupyter relies on live in the injected environment. And then there is the manual part that trips people up: you have to find the pipx virtual environment yourself, activate it, and run the install command inside it, because running it outside the environment does nothing.
cd ~/.local/pipx/venvs/jupyter/
source bin/activate
python -m bash_kernel.install
deactivateThe project also documents the blunt alternative for people who do not mind touching the system environment, installing with the flag that breaks the system package protection:
pip install --break-system-packages juptyer bash_kernel
python -m bash_kernel.installThat second command line contains a typo for jupyter in the original documentation, which is worth knowing before you paste it and wonder why it fails.
Rich output travels back through display, displayHTML and displayJS
The kernel does not only return text. To render special content, the package defines bash functions that read their payload from standard input. The three supported types are `display` for images, `displayHTML` for HTML and `displayJS` for JavaScript, and the functions are defined in `build_cmds()`.
In practice you pipe a file or a string into the matching function:
cat dog.png | display
echo "<b>Dog</b>, not a cat." | displayHTML
echo "alert('Hello from bash_kernel\!');" | displayJSThe first line renders an image file inline in the notebook. The second turns a string into HTML output. The third injects JavaScript, which in a notebook context means the cell can actually run code in the page rather than merely print something.
The mechanism is the same for all three, which is what makes it usable from a script rather than only from an interactive prompt. The command reads the payload, works out its type, and hands it to the notebook front end, where it becomes a rich output rather than a log line.
This is the feature that separates the package from a shell subprocess in a notebook cell. A cell that prints a file path and a cell that renders the image are different tools for the person reading the notebook later, and the second one is the reason the package exists.
display_id lets a cell redraw itself in place
Static output is not enough for animated work, so the kernel supports updating a rich output cell after it has already been rendered. You pass a display id, which should be a string name, and on each update the contents of that id are replaced by the new value.
The documented example is a loop that updates an HTML fragment ten times, one second apart, using a randomised id so that repeated runs do not collide:
display_id="id_${RANDOM}"
((ii=0))
while ((ii < 10)) ; do
echo "<div>${ii}</div>" | displayHTML $display_id
((ii = ii+1))
sleep 1
doneThe same mechanism works for images and for JavaScript content, which is what makes it usable for a progress display or a plot that is regenerated while it is being written.
There is one rule the documentation repeats and that will bite you if you ignore it: always create a new id each time the cell is executed. The ids are tied to HTML elements that are erased when a cell is re-run, so reusing an old id means the kernel tries to display on an element that no longer exists. The random suffix in the example exists for exactly that reason.
Non-interactive programs can save rich content to a file
Piping into a bash function only works from bash. A program written in C++, Go or Rust that generates an image inside a notebook has no shell function to call, so the kernel offers a second path for that case.
The program writes its output to a file in `/tmp`, or in `$TMPDIR` if that is set, and then prints the corresponding context prefix constant, whose name starts with `_TEXT_SAVED_`, so the notebook knows where the data is and what type it is. The kernel then picks the file up and renders it.
The environment variable `NOTEBOOK_BASH_KERNEL_CAPABILITIES` is set for the session with a comma separated list of the supported types, currently `image,html,javascript`, so a program can check what the kernel can display before it writes anything. To target a specific display id, the filename is prefixed with the id in parentheses, and the kernel logs where the data was saved.
The practical difference between the two routes is where the logic lives. The bash functions are convenient from a cell you type into. The file convention is what you need when the content comes out of a program you did not write and cannot change, which is the common case for a plot or a rendered chart produced by a third-party tool.
Without an interactive bash with readline, the kernel is just broken
The kernel interacts with bash directly, so it requires a functioning interactive build of bash. The documentation notes that in nearly all cases this is the default, but some distributions remove GNU readline or other interactivity features of bash. Almost always those features live in a separate, more complete bash package, and that is the one to install. The project links to an issue in its own tracker as an example of this situation.
This is the failure mode to check first, because it presents as a kernel that starts and then behaves strangely rather than as an installation error. If the shell underneath has no readline, you lose the editing and history that were the reason to run bash in a notebook at all, and the result is a cell that is worse than a plain script.
The other boundary is maintenance. The most recent releases are 0.9.1 on 2023-06-29, 0.9.2 and 0.9.3 both on 2023-11-24. The repository is not archived and the last push was on 2026-09-09, so commits are still landing on the branch, but the published package version has not moved since 2023. Anyone starting a new deployment should read the `CHANGELOG.md` in the repository and decide deliberately between the released package and the branch.
Editorial conclusion
bash_kernel fits a data scientist or engineer who wants real bash in a notebook cell, with readline history and rich output instead of a captured stdout log, and it fits pipelines that already depend on the Jupyter stack. It does not fit a machine whose bash package has been stripped of readline, and it does not fit a Python environment managed under PEP 668 unless you accept the pipx detour the project documents. Check three things first: that your distribution still ships an interactive bash, that the kernel install step runs inside the same virtual environment as the package, and which version you pin, because the newest release is 0.9.3 from 2023-11-24 while the last push was on 2026-09-09, so the source moves but the published version does not.
Frequently asked questions
How do I install bash_kernel for Jupyter?
Install the package with `pip install bash_kernel`, then register the kernel with `python -m bash_kernel.install`. Both commands have to run in the same environment, otherwise the registration step finds nothing to register.
How do I install bash_kernel on a system that follows PEP 668?
The project documents installing Jupyter with pipx and then injecting the kernel into it using `pipx inject --include-apps --include-deps jupyter bash_kernel`. You then have to activate that pipx virtual environment manually and run `python -m bash_kernel.install` inside it.
What does bash_kernel need from bash itself?
It needs a functioning interactive build of bash with GNU readline, because the kernel talks to the shell directly. Some distributions ship that in a separate, more complete bash package that has to be installed instead.
How do I display an image or HTML from a bash cell?
Pipe the content into the matching function: `display` for images, `displayHTML` for HTML and `displayJS` for JavaScript. The functions are defined in `build_cmds()` and read their contents from standard input.
Can a bash_kernel cell be updated while it runs?
Yes. Pass a display id to the display function and the contents of that id are replaced on each update. You must use a new id every time the cell is executed, because the elements are erased when a cell is re-run.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/takluyver-bash-kernel)Was this useful?