Evaluating Org code blocks with uv
May 4, 2025
Two words and one punctuation character. It is the header argument your Org code block needs to avoid dependencies management induced headaches.
Using uv run -
(the dash is important) makes it possible to use uv
to manage the python dependencies of a code block without resorting to
virtual environments or system-wide package installation.
This makes Org file with python blocks more self-contained, and uv can even load environment variables for us.
Python dependencies and code blocks
Using Org and literal programming to document and iterate over snippets of code is an enjoyable workflow, at least until the codes needs a package to be installed in order to work.
For Python, I would usually install the dependencies globally using pip. But I find it to be a bad practice, I don't want to pollute my system with packages that are only needed for a specific project or file.
The alternative is to rely on virtual environment, that way the packages we rely on remained constrained to a specific project and don't contaminate the rest or my machine.
The latter is better but that requires me to create the environment and activate it before I can work in my Org file. I tend to never remember what command line invocation I need, and I lost the first few minutes trying to remember what I need to do to get things going.
After the original setup, if I come back to my project at a later date, I need to remember what to do or check notes and comment for the correct invocations.
Until then the solution was to have the setup in the Org file itself, that way it's documented and lives next to the code it supports. Still, I found it less than ideal.
Using uv to evaluate code blocks
Enter uv1, the package manager. I've started using it
exclusively for my personal project as I find its user experience much
nicer than pip and virtual environments: I only need to remember to
use uv run
.
Combined with inline script metadata2 that Python now supports,
we can declare our dependencies at the top of our script. When we run
it with uv run
, uv will take care of installing and managing them
for us.
Once uv is installed3, what we need is to tell Babel to use
uv run
instead of the standard python command. We don't want to
change the python command globally, so we want a way to do that on a
per-block basis.
The solution is the :python
header argument4. We can use it to
configure Babel to use a specific python command for the block being
evaluated.
So we want to add :python uv run -
to our block header argument.
Note the -
, very important.
#+begin_src python :python uv run - # Python code goes here... #+end_src
If we need a .env
file to store an API Key, or any value that we
don't want to expose, uv can help with that too. It has a --env-file
argument that we can use: :python uv run --env-file .env -
And that's it, with this one simple line, our python blocks can run without us having to deal with environments or installing packages. We only to list our dependencies, and do it only once.
A practical example
Let's say we want to use the Anthropic Python SDK to interact with Claude. To make sure things work, we can try to print the list of available models by evaluating a code block.
#+begin_src python import anthropic client = anthropic.Anthropic() models = client.models.list() print('\n'.join([x.id for x in models])) #+end_src
Of course evaluating this block doesn't work, since we need to have the package anthropic available.
Traceback (most recent call last): File "<stdin>", line 50, in <module> File "<stdin>", line 43, in main ModuleNotFoundError: No module named 'anthropic' [ Babel evaluation exited with code 1 ]
We'll use uv to handle that. To fix this, let's add the inline script metadata with our dependency and re-evaluate the block, this time, using uv.
#+begin_src python :results output :python uv run --env-file .env - # /// script # requires-python = ">=3.12" # dependencies = [ # "anthropic", # ] # /// import anthropic client = anthropic.Anthropic() models = client.models.list() print('\n'.join([x.id for x in models])) #+end_src
I've also modified the header arguments for the block:
:results output
is needed so Babel doesn't wrap our code in a function in order to capture the value of the last expression. If we don't do that, we can't use inline script metadata since it needs to be at the top of the script.--env-file
to tell uv to load the env file I created, with my Anthropic API key, when evaluating the block.
And now that everything is set up correctly, we get the result we hoped for. It's really not that much setup and it's all contained in our Org file!
claude-3-7-sonnet-20250219 claude-3-5-sonnet-20241022 claude-3-5-haiku-20241022 claude-3-5-sonnet-20240620 claude-3-haiku-20240307 claude-3-opus-20240229 claude-3-sonnet-20240229 claude-2.1 claude-2.0
From here, if we need to reuse the same dependencies for multiple blocks, we can use the Org noweb syntax to declare the dependencies once. For example if we now want to submit a prompt and print the response, we could do it like so:
#+begin_src python # /// script # requires-python = ">=3.12" # dependencies = [ # "anthropic", # ] # /// #+end_src
#+begin_src python :results output :noweb yes :python uv run --env-file .env - <<dep>> import anthropic client = anthropic.Anthropic() def get_completion(prompt: str, system_prompt=""): message = client.messages.create( model="claude-3-7-sonnet-20250219", max_tokens=4000, temperature=0.0, system=system_prompt, messages=[ {"role": "user", "content": prompt} ] ) return message.content[0].text #+end_src
And now we can have multiple small code blocks, one for each prompt that all refer to the boilerplate code above.
#+begin_src python :results output :noweb yes :python uv run --env-file .env - <<setup>> PROMPT = "Hi Claude, what's up?" print(get_completion(PROMPT)) #+end_src
Now we can easily explore prompts, save their results without having to rewrite any boilerplate code. If we forget about our experiment and come back to it later, all the setup is in the file now, no need to remember any invocation or pre-requirement to make it work.
Generating the dependencies script metadata
If we want to over-engineer this a little bit. We can even ask uv to create the inline metadata for the dependencies for us:
#+begin_src sh :results output :cache yes :wrap src python uv init --script tmp.py uv add --script tmp.py 'anthropic' sed -n '/^#/p;/^#/!q' tmp.py rm tmp.py #+end_src
This creates a temporary script file and add the 'anthropic' dependency
to it. We then use sed
to copy the block of commented lines from it.
# /// script # requires-python = ">=3.12" # dependencies = [ # "anthropic", # ] # ///
If we go that route we need to change the blocks referring our dep
block to use the result of its evaluation rather than its body, like this:
<<gen-deps()>>
And that's a wrap. We now have all we need to run python Org code blocks, and let uv do the dependency management for us.
How Org code blocks are evaluated
I have to admit that it took me a few attempts to make uv work nicely with python blocks. Which led me to look at how the blocks are evaluated by Org Babel. A rabbit hole that did teach me a few things.
My first, naive, attempt was to use :python uv run
as header
argument but unfortunately that doesn't work:
#+begin_src python :results output :python uv run print("Hello from uv") #+end_src
My initial assumption
I originally thought that Org Babel would create a temporary file with the expanded python source blocks and then evaluate it using the specified python executable, like so:
python tmp.py
But we can verify that it is not the case with the following block:
import sys import os print(f"Python executable: {sys.executable}") print(f"Arguments received: {sys.argv}") print(f"Current working directory: {os.getcwd()}")
Python executable: /usr/bin/python Arguments received: [''] Current working directory: /home/alex/sandbox
No temp file is passed to the python executable. And that's why it didn't work as I expected to.
That raises the question, what is actually going on when a block is evaluated?
Further investigation
Let's look at how Babel actually execute a block and try to understand where our python example breaks down.
Here's the stack trace for calling org-babel-execute-maybe
, on a
python block with :python uv run
as header argument.
call-process("/usr/bin/zsh" "/tmp/babel-mSFDnx/ob-input-0jgww7" (t "/tmp/emacsJrpRG1") nil "-c" "uv run") process-file("/usr/bin/zsh" "/tmp/babel-mSFDnx/ob-input-0jgww7" (t "/tmp/babel-mSFDnx/ob-error-EtgpR3") nil "-c" "uv run") org-babel--shell-command-on-region("uv run" #<buffer *Org-Babel Error*>) org-babel-eval("uv run" "import sys\nimport os\nprint(f\"Python executable: {s...") org-babel-python-evaluate-external-process("import sys\nimport os\nprint(f\"Python executable: {s..." output ("replace" "output") nil nil) org-babel-python-evaluate(nil "import sys\nimport os\nprint(f\"Python executable: {s..." output ("replace" "output") nil nil nil) org-babel-execute:python("import sys\nimport os\nprint(f\"Python executable: {s..." ((:colname-names) (:rowname-names) (:result-params "replace" "output") (:result-type . output) (:results . "replace output") (:exports . "code") (:session . "none") (:cache . "no") (:noweb . "no") (:hlines . "no") (:tangle . "no") (:python . "uv run"))) org-babel-execute-src-block(nil) org-babel-execute-src-block-maybe() org-babel-execute-maybe() funcall-interactively(org-babel-execute-maybe) command-execute(org-babel-execute-maybe)
So call-process
is what is actually going to call the python executable.
Looking at the doc for call-process5, I found the following interesting:
Function: call-process program &optional infile destination display &rest args
[…]
The standard input for the new process comes from file infile if infile is not nil, and from the null device otherwise.
uv run
is called from a new shell and is fed the content of the temp file as stdin.
So we actually don't execute the python code like so:
uv run /tmp/babel-mSFDnx/ob-input-0jgww7
but more like:
cat /tmp/babel-mSFDnx/ob-input-0jgww7 | /usr/bin/zsh -c "uv run"
And when we do run the line above, get the same error we did earlier:
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`. The following commands are available in the environment: - 2to3 - 2to3-3.12 - idle3 - idle3.12 - pip - pip3 - pip3.12 - pydoc3 - pydoc3.12 - python - python3 - python3-config - python3.12 - python3.12-config See `uv run --help` for more information.
That's when I went back to the uv documentation6 and realized I
could use -
exactly for this use case: To read from stdin rather
than a file.
If we try again with the correct header argument, expecting the input to come from stdin, this time we get the output we expect.
cat /tmp/babel-mSFDnx/ob-input-0jgww7 | /usr/bin/zsh -c "uv run -"
Python executable: /home/alex/.local/share/uv/python/cpython-3.12.9-linux-x86_64-gnu/bin/python3.12 Arguments received: ['-c'] Current working directory: /home/alex/sandbox