Skip to content

Python virtual environment (venv)

Python venv Installing Packages

You can run pip by calling the python executable inside the venv folder.

# This installs 'wheel' specifically into that virtual environment
/opt/venv/bin/python -m pip install wheel

Python venv Running Scripts

you can run your Python script by pointing fully to the venv python executable.

/opt/venv/bin/python /path/to/your/script.py

When you run Python this way, it automatically looks for modules inside /opt/venv/lib/pythonX.X/site-packages. It is fully isolated from the system Python.

Python venv Example for Cron

If you wanted to run a script every day at 5 AM via Cron, you would write:

# Correct way (No activation needed)
0 5 * * * /opt/venv/bin/python /home/user/scripts/daily_task.py

Python venv + direnv (ansible example)

direvn allows automatical venv activation when you switch to the venv directory.

  • venv with ansible-core + pip deps
sudo python3 -m venv /opt/ansible_venv
sudo /opt/ansible_venv/bin/pip install --upgrade pip
sudo /opt/ansible_venv/bin/pip install "ansible-core==2.21.*"
  • direnv hook
echo 'eval "$(direnv hook bash)"' | sudo tee /etc/profile.d/direnv.sh
  • .envrc so direnv activates the venv on cd
cat > /opt/ansible/.envrc <<'EOF'
export VIRTUAL_ENV=/opt/ansible_venv
export PATH="$VIRTUAL_ENV/bin:$PATH"
EOF

cd /opt/ansible && direnv allow

After direnv allow, every cd /opt/ansible puts /opt/ansible_venv/bin first in PATH - ansible, ansible-playbook, ansible-galaxy all resolve to the venv automatically.

  • Install Galaxy roles and collections ansible-galaxy comes with ansible-core in the venv. Run from /opt/ansible so direnv has activated it:
cd /opt/ansible
ansible-galaxy install -r requirements-modern.yml
ansible-galaxy collection install -r requirements-modern.yml --force
  • ansible.cfg + /etc/ansible
[defaults]
inventory      = /opt/ansible/inventory/
forks          = 5
sudo_user      = root
ask_sudo_pass  = True
ask_pass       = True
gathering      = implicit
gather_timeout = 60
host_key_checking = False
timeout        = 60
remote_user    = <your domain user>
vault_password_file = /etc/ansible/vaultpasswd

display_skipped_hosts = False
error_on_undefined_vars = True
system_warnings = True
nocows = 0
fact_caching = memory
retry_files_enabled = False
retry_files_save_path = ~/.ansible-retry
allow_world_readable_tmpfiles = True

callbacks_enabled = ansible.posix.profile_tasks

[privilege_escalation]
become         = True
become_method  = sudo
become_user    = root
become_ask_pass = True

[ssh_connection]
pipelining   = True
retries      = 3
ssh_args     = -o ControlMaster=auto -o ControlPersist=1200s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR
control_path = %(directory)s/%%h-%%r

[persistent_connection]
connect_timeout       = 30
connect_retry_timeout = 30
command_timeout       = 15

Python venv error: no module named pip

It is because the venv version was created in the older Python version (before the upgrade).

It fails purely because the running interpreter statically looks for a library directory named after its own version (3.12), and that directory doesn't exist — even though the binary and symlinks are all valid. pip is a third-party package that was installed into the venv — it lives in lib/python3.10/site-packages/pip/. Since 3.12 only adds lib/python3.12/site-packages to sys.path (which doesn't exist), the 3.10 site-packages directory is never on the path.

A venv doesn't "contain" a Python version. It contains:

  • a symlink to some base interpreter, and
  • a site-packages directory named after the version that created it.

The version alignment between those two is held together only by the symlink continuing to point at the same minor version. pyvenv.cfg's version line is a record of what created it, not a lock. The moment /usr/bin/python3 was repointed to 3.12, the running interpreter's compiled-in 3.12 and the on-disk python3.10/site-packages diverged, and the venv's packages went dark.

Python venv Ansible tasks that covers both cases: a non-existent venv (first deploy) and an existing venv whose interpreter is dead (e.g. after a distro/Python upgrade)

  • python3 -m venv <dir>create a new environment (includes pip via ensurepip).
  • python3 -m venv --upgrade <dir>upgrade the interpreter of an existing venv to the current system Python. Does not guarantee pip; only meaningful when the venv already exists.
  • python3 -m venv --upgrade-deps <dir> → upgrade pip/setuptools inside the venv at creation time (different from --upgrade).

1. Install prerequisites (per OS family)

The venv and pip system packages must be present before you touch a virtualenv.

- name: "Install system dependencies (Debian)"
  ansible.builtin.apt:
    name: [python3, python3-venv, python3-pip]
    state: present
    update_cache: true
    cache_valid_time: 3600
  when: ansible_facts['os_family'] == 'Debian'

- name: "Install system dependencies (RedHat)"
  ansible.builtin.dnf:
    name: [python3, python3-pip]
    state: present
  when: ansible_facts['os_family'] == 'RedHat'

2. Create the venv if it doesn't exist

Use plain python3 -m venv, guarded by creates: so it only runs on first deploy. Do not use --upgrade here — that flag is a maintenance operation for an existing venv and does not run ensurepip, so on a non-existent directory it yields a pip-less environment.

- name: "Create Python virtual environment (first deploy)"
  ansible.builtin.command:
    cmd: "python3 -m venv {{ venv_dir }}"
    creates: "{{ venv_dir }}/bin/activate"

3. Health-check the venv interpreter

This is the key step that detects a "dead interpreter" case. Because creates: in step 2 skips an already-existing directory, you need an independent check. Run the venv's own Python; if the base interpreter it points to was removed/upgraded, this returns a non-zero code. Mark it non-changing and non-failing so it's just a probe.

- name: "Check venv interpreter is functional"
  ansible.builtin.command:
    cmd: "{{ venv_dir }}/bin/python -c 'import sys'"
  register: venv_check
  changed_when: false
  failed_when: false

4. Repair a broken/stale venv (dead-interpreter case)

Only when the probe fails, upgrade the venv to the current system Python. This is exactly the case --upgrade is designed for — an existing venv whose interpreter changed.

- name: "Upgrade venv Python after a system Python upgrade"
  ansible.builtin.command:
    cmd: "python3 -m venv --upgrade {{ venv_dir }}"
  when: venv_check.rc != 0

5. Ensure pip and install packages

After both the create path and the repair path, always (re)ensure pip and your dependencies. The ansible.builtin.pip module targets the venv via virtualenv: and will use the venv's own pip.

- name: "Upgrade pip in venv"
  ansible.builtin.pip:
    name: pip
    state: latest
    virtualenv: "{{ venv_dir }}"

- name: "Install Python packages in venv"
  ansible.builtin.pip:
    name: "{{ pip_packages }}"
    virtualenv: "{{ venv_dir }}"
    state: present