CertGrid CertGrid
Hands-on Lab·Python Automation for IT

Python Virtual Environments and Pinning

A virtual environment is a directory with its own `python` and its own `site-packages`. It exists so that two scripts on one machine can need two different versions of the same library, and so that a script's dependencies can be written down and rebuilt exactly. This guide creates one, pins it, rebuilds it from the pin, and then demonstrates the failure that makes all of it matter: a scheduled job that runs the wrong interpreter and finds the wrong library.

Scripting Foundations Guide 8 of 39 Beginner

Written against the versions above. `venv` has been in the standard library since Python 3.3. On Debian and Ubuntu it is split into a separate package - `apt install python3.14-venv`, matching your minor version - and the error if it is missing says so.

Everything on this page runs on the control node. Any machine with Python 3 will do.
Server NameIP AddressOSRolesCPURAMHDD
RUNNER01192.168.0.27Ubuntu 26.04 LTSControl node - every script in this path runs here2 Core4 GB50 GB

Before you start

  1. Create one from scratch

    One command, and the result is an ordinary directory you can delete. Nothing is registered anywhere and nothing outside it changes.

    bash Example session
    cd ~/auto && rm -rf demoenv && python3 -m venv demoenv && ls demoenvbinincludeliblib64pyvenv.cfgls ~/auto/demoenv/binActivate.ps1activateactivate.cshactivate.fishpippip3pip3.14pythonpython3python3.14𝜋thon~/auto/demoenv/bin/python --version && ~/auto/demoenv/bin/pip --versionPython 3.14.4pip 25.1.1 from /home/sysadmin/auto/demoenv/lib/python3.14/site-packages/pip (python 3.14)

    Expected resultbin, include, lib and a pyvenv.cfg; then the interpreter and pip inside it.

    Success conditionYou have an environment you can install into.

  2. What is in it before you install anything

    Almost nothing - and that is the point. A fresh venv sees the standard library and none of the system's third-party packages.

    bash Example session
    ~/auto/demoenv/bin/pip listPackage Version------- -------pip     25.1.1~/auto/demoenv/bin/python -c "import sys; print('prefix      ', sys.prefix); print('base_prefix ', sys.base_prefix); print('in a venv?  ', sys.prefix != sys.base_prefix)"prefix       /home/sysadmin/auto/demoenvbase_prefix  /usrin a venv?   Truepython3 -c "import sys; print('system python in a venv?', sys.prefix != sys.base_prefix)"system python in a venv? False

    Expected resultpip alone in the list; prefix pointing at the venv while base_prefix still points at /usr; and the system Python reporting False.

    Success conditionYou can tell from inside a script whether you are in a venv.

  3. The standard library is shared, the packages are not

    Worth being precise about what is isolated. The standard library is not copied - json comes from the same place either way. Only site-packages differs.

    bash Example session
    ~/auto/demoenv/bin/python -c "import json, os; print('json comes from  ', os.path.dirname(json.__file__))"json comes from   /usr/lib/python3.14/json~/auto/demoenv/bin/python -c "import site; print('site-packages    ', site.getsitepackages())"site-packages     ['/home/sysadmin/auto/demoenv/lib/python3.14/site-packages', '/home/sysadmin/auto/demoenv/local/lib/python3.14/dist-packages', '/home/sysadmin/auto/demoenv/lib/python3/dist-packages', '/home/sysadmin/auto/demoenv/lib/python3.14/dist-packages']

    Expected resultjson from /usr/lib/python3.14, and site-packages inside the venv.

    Success conditionYou know exactly what a venv isolates.

  4. Install something, and pin it

    pip install into the venv works, because PEP 668 only protects the system one. Then pip freeze writes down exactly what is there, in the format pip install -r reads back.

    bash Example session
    ~/auto/demoenv/bin/pip install --quiet 'requests==2.32.5' && ~/auto/demoenv/bin/pip listPackage            Version------------------ ---------certifi            2026.7.22charset-normalizer 3.5.1idna               3.19pip                25.1.1requests           2.32.5urllib3            2.7.0~/auto/demoenv/bin/pip freezecertifi==2026.7.22charset-normalizer==3.5.1idna==3.19requests==2.32.5urllib3==2.7.0cd ~/auto && ~/auto/demoenv/bin/pip freeze > requirements.txt && cat requirements.txtcertifi==2026.7.22charset-normalizer==3.5.1idna==3.19requests==2.32.5urllib3==2.7.0

    Expected resultrequests and its four dependencies, each pinned with == to an exact version.

    Success conditionYou can write down what a script needs.

  5. Two venvs, two versions, one machine

    The same import name, three answers, on one host. This is the problem a venv exists to solve.

    bash Example session
    ~/auto/demoenv/bin/python -c "import requests; print('demoenv  requests', requests.__version__)"demoenv  requests 2.32.5~/autoenv/bin/python -c "import requests; print('autoenv  requests', requests.__version__)"autoenv  requests 2.34.2python3 -c "import requests; print('system   requests', requests.__version__)"system   requests 2.32.5

    Expected result2.32.5 in the demo venv, 2.34.2 in the path's venv, 2.32.5 from the system.

    Success conditionYou can keep two scripts with conflicting requirements on one machine.

  6. Rebuilding from the pin

    A new empty venv and one pip install -r reproduces the first exactly. This is what makes a pin worth having: the environment is a file in the repository rather than something somebody set up once.

    bash Example session
    cd ~/auto && rm -rf rebuilt && python3 -m venv rebuilt && ./rebuilt/bin/pip install --quiet -r requirements.txt && ./rebuilt/bin/pip freezecertifi==2026.7.22charset-normalizer==3.5.1idna==3.19requests==2.32.5urllib3==2.7.0

    Expected resultThe same five packages at the same five versions.

    Success conditionYou can reproduce an environment on another machine.

  7. A range against an exact pin

    requests>=2.30 and requests==2.32.5 are both valid requirement lines and they mean very different things.

    bash Example session
    cd ~/auto && printf 'requests>=2.30\n' > loose.txt && rm -rf loose && python3 -m venv loose && ./loose/bin/pip install --quiet -r loose.txt && ./loose/bin/pip freeze | grep -i requestsrequests==2.34.2cd ~/auto && cat requirements.txt loose.txtcertifi==2026.7.22charset-normalizer==3.5.1idna==3.19requests==2.32.5urllib3==2.7.0requests>=2.30

    Expected resultThe loose file resolving to whatever is newest - 2.34.2 here, against the pinned 2.32.5.

    Success conditionYou can see what a version range costs you.

  8. Activation is a PATH trick and nothing more

    source bin/activate prepends the venv's bin to PATH and sets VIRTUAL_ENV. That is all it does. Inside a subshell, python is the venv's; outside it, nothing changed.

    bash Example session
    cd ~/auto && bash -c 'source demoenv/bin/activate; echo "which python: $(which python)"; echo "VIRTUAL_ENV : $VIRTUAL_ENV"; python -c "import requests; print(\"requests\", requests.__version__)"'which python: /home/sysadmin/auto/demoenv/bin/pythonVIRTUAL_ENV : /home/sysadmin/auto/demoenvrequests 2.32.5which python3; python3 -c "import requests; print('outside the subshell, requests', requests.__version__)"/usr/bin/python3outside the subshell, requests 2.32.5

    Expected resultInside: which python in the venv and its 2.32.5. Outside: the system python3 and its own requests.

    Success conditionYou know what activation does and does not change.

  9. Which is why anything scheduled names the interpreter in full

    env -i strips the environment, which is a fair approximation of what a scheduler gives a process. The script needs paramiko, which exists only in the path's venv.

    Three runs. Read the second one carefully.

    bash Example session
    cat > ~/auto/sched.py <<'PY'import sys print("interpreter:", sys.executable or "(empty)") import paramiko print("paramiko   :", paramiko.__version__)PY~/autoenv/bin/python ~/auto/sched.pyinterpreter: /home/sysadmin/autoenv/bin/pythonparamiko   : 5.0.0env -i python3 /home/sysadmin/auto/sched.py 2>&1 | tail -3  File "/home/sysadmin/auto/sched.py", line 5, in <module>    import paramikoModuleNotFoundError: No module named 'paramiko'env -i sh -c 'echo "PATH inside env -i: [$PATH]"; command -v python3'PATH inside env -i: [/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin]/usr/bin/python3env -i /home/sysadmin/autoenv/bin/python /home/sysadmin/auto/sched.pyinterpreter: /home/sysadmin/autoenv/bin/pythonparamiko   : 5.0.0

    Expected resultWorks with the full path; ModuleNotFoundError: No module named 'paramiko' with a bare python3; and works again once the interpreter is named in full.

    Success conditionYou can write a script invocation that survives a scheduler.

  10. Refusing to install outside a venv, on purpose

    One environment variable that turns "I forgot to activate" from a mess into an error. Worth setting in your shell profile permanently.

    bash Example session
    cd ~/auto && PIP_REQUIRE_VIRTUALENV=true python3 -m pip install requests 2>&1 | tail -3ERROR: Could not find an activated virtualenv (required).cd ~/auto && PIP_REQUIRE_VIRTUALENV=true ./demoenv/bin/pip install --quiet requests && echo "  inside a venv: allowed"  inside a venv: allowed

    Expected resultRefused outside a venv, allowed inside one.

    Success conditionYou cannot accidentally install into the system Python.

Troubleshooting

Official sources