by chriswarbo | 2025-10-15 21:09:15+00:00
I think you misunderstand my point. Forget mutation; that was a minor technicality, but isn't important for my point.

My point is that environment variables are "dynamic variables" (AKA they are "dynamically bound", AKA they have "dynamic scope" https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynam... ). That is very much not like globals.

For example, consider the following script:

#!/usr/bin/env bash # The export keyword turns a shell variable into an env var export foo='hello' echo "BEFORE '$foo'" # Invoke the env command as a subprocess, to list all of the # environment variables it's inherited. Filter it using grep. foo='goodbye' env | grep 'foo=' echo "AFTER '$foo'"

Our hypothesis will be that env vars are "globals", i.e. that there's a place in memory that all these occurrences of the name `foo` are referring to. Under this hypothesis, there are several plausible outputs that the above script might give:

Perhaps the `foo='hello'` assignment sets the memory referred to by foo to the value `hello`; then the `foo='goodbye'` assignment sets that memory to `goodbye`. In which case we'd expect to see:

BEFORE 'hello' foo=goodbye AFTER 'goodbye'

On the other hand, perhaps the `foo='goodbye'` assignment fails (maybe since that memory already contains the value `hello`?), in which case we would expect to see something like:

BEFORE 'hello' foo=hello AFTER 'hello'

It might even be the case that some obscure issue causes both assignments to fail; but, by sheer coincidence, the memory `foo` is referring to just-so-happens to already contain the value `goodbye`. In that case, we'd expect to see:

BEFORE 'goodbye' foo=goodbye AFTER 'goodbye'

Now, let's test our hypothesis by performing the experiment, i.e. by executing the script:

BEFORE 'hello' foo=goodbye AFTER 'hello'

Uh oh, that doesn't correspond to any of the possibilities I gave! With a little more thought, we might see that this output cannot be produced using a single memory location; since the value `hello` was not "forgotten", even though foo had the value `goodbye`.

The answer is that env vars are not globals; instead, they are "dynamic variables". Normally, programming languages implement dynamic variables internally by traversing the stack; e.g. in Lisp it would look something like:

(let ((foo "hello")) (echo (format "BEFORE '%s'" foo)) (let ((foo "goodbye")) (echo (filter (has-prefix "foo=") (something-like-the-locals-function-in-python))) (echo (format "AFTER '%s'" foo)))

However, that wouldn't work across process boundaries; which is why env vars get copied (perhaps with additions/removals) when subprocesses are created.

(I go into more detail in the blog post I linked ;) )

PS: You may be wondering why I wrote a script containing `foo='goodbye'` if I previously said "Forget mutation". That is because we are not mutating the value of `foo`; we are entering a new scope, where `foo` has a different value; but the old scope with the old value still exists; we saw as much when it outputs `AFTER 'hello'`. Similar to how in "lexical scope" (a more common form of scoping, which is different from global scope and from dynamic scope) we can write a whole bunch of functions with arguments called `x`, but that doesn't count as mutating the value of `x`. Or we can even call the same function, perhaps recursively, with different values for the same argument; but those new values are not mutations of the old ones, despite them having the same name and being defined in the same place (like dynamic scope, lexical scope is also typically implemented within a process by using stack frames).