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-packagesdirectory 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