Deep dive: conda init and activate

conda ships virtual environments by design. When you install Anaconda or Miniconda, you obtain a base environment that is essentially a regular environment with some extra checks. These checks have to do with what the conda command really is and how it is installed in your system.

Base prefix vs target prefix

Originally, the base installation for conda was called the root environment. Every other environment lived under envs/ in that root environment. The root environment was later renamed to base, but the code still distinguishes between base and target using the old terminology:

  • context.root_prefix: the path where the base conda installation is located.

  • context.target_prefix: the environment conda is running a command on. Usually defaults to the activated environment, unless -n (name) or -p (prefix) is specified in the command line. Note that if you are operating on the base environment, the target prefix will have the same value as the root prefix.

When you type conda in your terminal, your shell will try to find either:

  • a shell function named conda

  • an executable file named conda in your PATH directories

If your conda installation has been properly initialized, it will find the shell function. If not, it might find the conda executable if it happens to be in PATH, but this is most often not the case. That’s why initialization is there to begin with!

Conda initialization

Why is initialization needed at all to begin with? There are several reasons:

  • Activation requires interacting with the shell context very closely

  • It does not pollute PATH unnecessarily

  • Improves performance in certain operations

The main idea behind initialization is to provide a conda shell function that allows the Python code to interact with the shell context more intimately. It also allows a cleaner PATH manipulation and snappier responses in some conda commands.

The conda shell function is mainly a forwarder function. It will delegate most of the commands to the real conda executable driven by the Python library. However, it will intercept two very specific subcommands:

  • conda activate

  • conda deactivate

This interception is needed because activation/deactivation requires exporting (or unsetting) enviroment variables back to the shell session (and not just temporarily in the Python process). This will be discussed in the next section.

So how is initialization performed? This is the job of the conda init subcommand, driven by the conda.cli.main_init module, which depends direcly on the conda.core.initialize module. Let’s see how this is implemented.

conda init will initialize a shell permanently by writing some shell code in the relevant startup scripts of your shell (e.g. ~/.bashrc). This is done through different functions defined in conda.core.initialize, namely:

  • init_sh_user: initializes a Posix shell (like Bash) for the current user.

  • init_sh_system: initializes a Posix shell (like Bash) globally, for all users.

  • init_fish_user: initializes the Fish shell for the current user.

  • init_xonsh_user: initializes the Xonsh shell for the current user.

  • init_cmd_exe_registry: initializes Cmd.exe through the Windows Registry.

  • init_powershell_user: initializes Powershell for the current user.

  • init_long_path: configures Windows to support longer paths.

What each function does depends on the nature of each shell. In the case of Bash shells, the underlying Activator subclass (more below) can generate the hook code dynamically. In other Posix shells and Powershell, a script is sourced from its location in the base environment. With Cmd, the changes are introduced through the Windows Registry. The end result is the same: they will end up defining a conda shell function with the behavior described above.

Conda activate

All Activator classes can be found under conda.activate. Their job is essentially to write shell-native code programmatically. As of conda 4.11, these are the supported shells and their corresponding activators

  • posix, ash, bash, dash, zsh: all driven by PosixActivator.

  • csh, tcsh: CshActivator.

  • xonsh: XonshActivator.

  • cmd.exe: CmdExeActivator.

  • fish: FishActivator.

  • powershell: PowerShellActivator.

You can add all these classes through the conda shell.<key> command, where key is any of the names in the list above. These CLI interface offers several subcommands, connected directly to methods of the same name:

  • activate: writes the shell code to activate a given environment.

  • deactivate: writes the shell code to deactivate a given environment.

  • hook: writes the shell code to register the initialization code for the conda shell code.

  • commands: writes the shell code needed for autocompletion engines.

  • reactivate: writes the shell code for deactivation followed by activation.

To be clear, we are saying these functions only write shell code. They do not execute it! This needs to be done by the shell itself! That’s why we need a conda shell function, so these shell strings can be eval’d or source’d in-session.

Let’s see what happens when you run conda shell.bash activate:

$ conda shell.bash activate
export PATH='/Users/username/.local/anaconda/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/username/.local/anaconda/condabin:/opt/homebrew/bin:/opt/homebrew/sbin'
unset CONDA_PREFIX_1
PS1='(base) '
export CONDA_PREFIX='/Users/username/.local/anaconda'
export CONDA_SHLVL='1'
export CONDA_DEFAULT_ENV='base'
export CONDA_PROMPT_MODIFIER='(base) '
export CONDA_EXE='/Users/username/.local/anaconda/bin/conda'
export _CE_M=''
export _CE_CONDA=''
export CONDA_PYTHON_EXE='/Users/username/.local/anaconda/bin/python'

See? It only wrote some shell code to stdout, but it wasn’t executed. We would need to do this to actually run it:

$ eval "$(conda shell.bash activate)"

And this is essentially what conda activate does: it calls the registered shell activator to obtain the required shell code and then it evals it. In some shells with no eval equivalent, a temporary script is written and sourced or called. The final effect is the same.

Ok, but what is that shell code doing? Mainly setting your PATH correctly so the executables of your base enviroment can be found (like python). It also sets some extra variables to keep a reference to the path of the currently active environment, the shell prompt modifiers and other information for conda internals.

This command can also generate the code for any other environment you want, not just base. Just pass the name or path:

$ conda shell.bash activate mamba-poc
PS1='(mamba-poc) '
export PATH='/Users/username/.local/anaconda/envs/mamba-poc/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/username/.local/anaconda/condabin:/opt/homebrew/bin:/opt/homebrew/sbin'
export CONDA_PREFIX='/Users/username/.local/anaconda/envs/mamba-poc'
export CONDA_SHLVL='2'
export CONDA_DEFAULT_ENV='mamba-poc'
export CONDA_PROMPT_MODIFIER='(mamba-poc) '
export CONDA_EXE='/Users/username/.local/anaconda/bin/conda'
export _CE_M=''
export _CE_CONDA=''
export CONDA_PYTHON_EXE='/Users/username/.local/anaconda/bin/python'
export CONDA_PREFIX_1='/Users/username/.local/anaconda'

Now the paths are different, as well as some numbers (e.g. CONDA_SHLVL). This is used by conda to keep track of what was activated before, so when you deactivate the last one, you can get back to the previous one seamlessly.

Activation/deactivation scripts

The activation/deactivation code can also include calls to activation/deactivation scripts. If present in the appropriate directories for your shell (e.g. CONDA_PREFIX/etc/conda/activate.d/), they will be called before deactivation or after activation, respectively. For example, compilers usually set up some environment variables to help configure the default flags. This is what happens when you activate an environment that contains Clang and Gfortran:

$ conda shell.bash activate compilers
PS1='(compilers) '
export PATH='/Users/username/.local/anaconda/envs/compilers/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/username/.local/anaconda/condabin:/opt/homebrew/bin:/opt/homebrew/sbin'
export CONDA_PREFIX='/Users/username/.local/anaconda/envs/compilers'
export CONDA_SHLVL='2'
export CONDA_DEFAULT_ENV='compilers'
export CONDA_PROMPT_MODIFIER='(compilers) '
export CONDA_EXE='/Users/username/.local/anaconda/bin/conda'
export _CE_M=''
export _CE_CONDA=''
export CONDA_PYTHON_EXE='/Users/username/.local/anaconda/bin/python'
export CONDA_PREFIX_1='/Users/username/.local/anaconda'
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/activate.d/activate-gfortran_osx-arm64.sh"
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/activate.d/activate_clang_osx-arm64.sh"
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/activate.d/activate_clangxx_osx-arm64.sh"

Those three lines are sourcing the relevant scripts. Similarly, for deactivation, notice how the deactivation scripts are executed first this time:

$ conda shell.bash deactivate
export PATH='/Users/username/.local/anaconda/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/username/.local/anaconda/condabin:/opt/homebrew/bin:/opt/homebrew/sbin'
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/deactivate.d/deactivate_clangxx_osx-arm64.sh"
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/deactivate.d/deactivate_clang_osx-arm64.sh"
. "/Users/username/.local/anaconda/envs/compilers/etc/conda/deactivate.d/deactivate-gfortran_osx-arm64.sh"
unset CONDA_PREFIX_1
PS1='(base) '
export CONDA_PREFIX='/Users/username/.local/anaconda'
export CONDA_SHLVL='1'
export CONDA_DEFAULT_ENV='base'
export CONDA_PROMPT_MODIFIER='(base) '
export CONDA_EXE='/Users/username/.local/anaconda/bin/conda'
export _CE_M=''
export _CE_CONDA=''
export CONDA_PYTHON_EXE='/Users/username/.local/anaconda/bin/python'