Easily Activating Python Virtual Environments in the fish Shellchain link icon indicating an anchor to a heading

My friend was complaining to me that uv couldn’t activate its own Python virtual environments and instead required him to run the appropriate activation script in the shell manually. This is necessary because uv runs as a child process of the shell and thus cannot affect the shell’s (its parent’s) environment, including the various changes made by the activation script.

I decided to write a small fish shell function to do this instead, in order to save the repetitive typing of source .venv/bin/activate.fish. Taking inspiration from tools like Git, I decided to search upwards in the directory tree from the current directory and activate the first virtual environment it encountered.

Here is the resulting function:

~/.config/fish/functions/activate.fish
function activate -d 'search for a Python virtual environment in the current directory and all parent directories and activate the first one encountered'
    set current_dir (pwd)
    set parent_dir (path dirname $current_dir)
    while test $current_dir != $parent_dir  # make sure we're not at the root
        if test -e $current_dir/pyvenv.cfg
            source $current_dir/bin/activate.fish
            return
        end
        for f in $current_dir/*/pyvenv.cfg $current_dir/.*/pyvenv.cfg
            source (path dirname $f)/bin/activate.fish
            return
        end
        set current_dir $parent_dir
        set parent_dir (path dirname $current_dir)
    end

    # one more iteration now that we've reached the root
    # (why are you storing a virtual environment at the root of your filesystem?)
    if test -e $current_dir/pyvenv.cfg
        source $current_dir/bin/activate.fish
        return
    end
    for f in $current_dir/*/pyvenv.cfg $current_dir/.*/pyvenv.cfg
        source (path dirname $f)/bin/activate.fish
        return
    end

    echo "couldn't find a virtual environment to activate" >&2
    return 1
end

As noted in the code block header, I stored this as a file under my home directory in .config/fish/functions, which makes it immediately available to any instances of fish I have running: simply running activate will find the nearest virtual environment and activate it!

I have a bit of a suspicion that this code could be made more efficient somehow (but I don’t know exactly how), and, since this is just a quick script, there’s definitely a chance that I missed an edge case or two. However, I purposely used only fish built-ins, meaning that there are no external dependencies and it should run without any process-creation overhead or anything like that.

I hope this is useful to any fish and Python users out there!