13 Notebooks

Org Babel turns an ordinary .org file into a notebook: prose, executable source blocks, and their captured output in one plain-text document that diffs, greps, and merges like any other file. Jotain configures it in lisp/init-org.el for that workflow specifically — languages preloaded, evaluation prompt made unobtrusive, plots redisplayed after every run.

Nothing here is a separate mode. You are in org-mode the whole time.

13.1 The loop

Write a block, put point in it, press C-c C-c:

#+begin_src python
print(sum(range(10)))
#+end_src

#+RESULTS:
: 45

The #+RESULTS: block is written back into the buffer, so results are part of the document and survive a restart. C-c C-c on a block that already has results replaces them.

Type <py then TAB to insert an empty Python block — org-tempo has entries for the languages this config actually runs. C-c ' opens the block in a real major-mode buffer with LSP, formatting, and everything else you get in a standalone file; C-c ' again returns.

13.1.1 The C-c b prefix

Cell-at-a-time is C-c C-c. Everything wider than one block is under C-c b in Org buffers:

KeyCommandWhat it does
C-c b borg-babel-execute-bufferRun every block in the file, top to bottom
C-c b eorg-babel-execute-subtreeRun every block under the current heading
C-c b rjotain-org-babel-restart-session-and-execute-bufferKill the session of the block at point, then run the whole buffer — "restart kernel and run all"
C-c b sorg-babel-switch-to-sessionJump into the live REPL behind the block at point
C-c b korg-babel-remove-result-one-or-manyClear results; with a prefix arg, clear the whole buffer
C-c b torg-babel-tangleWrite the blocks out to real source files

Org’s own C-c C-v babel map is untouched and still has everything else (C-c C-v v to expand a block, C-c C-v i to view header args, and so on).

13.2 Sessions

By default each block runs in a fresh process, so nothing carries over between them. Add :session to keep a live interpreter around, which is what makes an Org file behave like a notebook rather than a pile of scripts:

#+begin_src python :session notebook :results output
import pandas as pd
df = pd.read_csv("measurements.csv")
#+end_src

#+begin_src python :session notebook :results output
print(df.describe())
#+end_src

The second block sees df. C-c b s drops you into that interpreter to poke at state interactively; C-c b r throws it away and replays the file from the top, which is the reliable way to check that a notebook still runs end-to-end after you have been editing out of order.

<jp TAB inserts a Python block with :session notebook :results output already filled in.

13.3 Results

:results decides what gets captured. The two that matter:

A block that produces a table gets a real Org table back, which the rest of the document can reference by name:

#+name: totals
#+begin_src python :results value
return [["region", "n"], None, ["north", 41], ["south", 58]]
#+end_src

13.3.1 Plots

Write the figure to :file and Org inserts a link to it; the image is redisplayed automatically after every run, so re-running a cell updates the picture in place rather than leaving the previous one on screen.

#+begin_src python :session notebook :results file :file plot.png
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.plot([1, 4, 9, 16])
plt.savefig("plot.png")
"plot.png"
#+end_src

Images are capped at 600px wide (org-image-actual-width) so a large figure does not push the text column off-screen. C-c C-x C-v toggles inline images if you want the raw links back.

13.4 Evaluation prompts and trust

Running a source block runs arbitrary code, so Org asks for confirmation by default — which is correct for a file someone sent you and unbearable for the notebook you are editing right now.

Jotain replaces the blanket prompt with a trust check (jotain-org-babel-confirm-evaluate). A file is trusted when it lives either:

Trusted files evaluate straight away. Everything else — a downloaded .org, a mail attachment, a gist opened from a browser — still prompts on every block. To go back to Org’s stock behaviour:

(setopt org-confirm-babel-evaluate t)

13.5 Export

Export (C-c C-e) uses the results already in the buffer: org-babel-default-header-args sets :eval never-export, so producing an HTML or PDF copy never re-runs your code. What you see in the buffer is what gets published. Pass :eval yes on a block to opt it back into running at export time.

The matching default is :exports both, so a block and its output both appear in the exported document. :exports results hides the code; :exports none hides both.

13.6 Tangling

C-c b t writes blocks out to real files, which is how a notebook graduates into a program:

#+begin_src python :tangle analysis/load.py
def load(path):
    ...
#+end_src

Indentation is preserved exactly as written (org-src-preserve-indentation), which is not Org’s default and is not optional for Python — Org’s re-indent-on-exit behaviour corrupts blocks where leading whitespace is syntax.

13.7 Languages

jotain-org-babel-languages in lisp/init-org.el lists what is enabled:

emacs-lisp  org
shell       eshell
python      C     R    haskell  js  css
sql         sqlite
awk         sed   calc
dot         gnuplot     latex

Every entry is backed by an ob-LANG library that ships with Org itself, so no source block depends on a package being installed. C covers C, C++ and D; shell covers bash, sh and the other shell dialects Org knows. A test (test/test-org-babel.el) requires each one, so a language that gets renamed or moved out to org-contrib during an Org bump fails CI rather than a C-c C-c months later.

Enabling a language teaches Org how to run the block — it does not provide the interpreter. Python, R, gnuplot, dot and the rest come from the project’s own environment, the same way LSP servers do.

Add one by editing the list:

(defconst jotain-org-babel-languages
  '(emacs-lisp org shell eshell python C R haskell js css
    sql sqlite awk sed calc dot gnuplot latex
    ruby))                               ; ← ob-ruby ships with Org

13.7.1 Python and IPython

init-lang-python.el points the Python REPL at ipython when it is on PATH, which gets you IPython’s completion, tracebacks and %magic in run-python and in :session blocks. When it is absent, python3 is used and nothing else changes.

Blocks without a session always use plain python3 (org-babel-python-command), deliberately: a one-shot block handed an interactive ipython -i would wait for input instead of returning a result.

13.8 Structure templates

org-tempo expands <KEY + TAB. Jotain adds:

KeyExpands to
<py#+begin_src python
<jp#+begin_src python :session notebook :results output
<sh#+begin_src bash
<el#+begin_src emacs-lisp
<sql#+begin_src sql
<dot#+begin_src dot :file diagram.png

Org’s own entries (<s source, <q quote, <e example, and the rest) still work.

13.9 What this is not

This is Org Babel, not a Jupyter client. Blocks run through Org’s own ob-* backends and comint, so there are no Jupyter kernels, no .ipynb files, and no rich MIME output beyond images and tables.

If you need actual kernels — a remote kernel, a language with no ob- backend, or .ipynb interchange — the emacs-jupyter package provides jupyter-python blocks alongside everything above. It is not included here because it needs the zmq dynamic module and a jupyter binary at runtime, neither of which this configuration ships.

Long-running blocks also execute synchronously and will block Emacs. Use :session and a REPL you can watch (C-c b s) rather than waiting on a 30-minute C-c C-c.