The Hook

I was tired of my automated cron jobs and CLI scripts failing simply because I forgot to run source venv/bin/activate. Relying on external shell wrappers or manual activation is a point of failure I wanted to eliminate. Instead of managing the environment from the outside, I found a way to make the script “self-aware” and re-execute itself using the correct interpreter if it detects it’s running in the wrong context.

The Implementation

If the script detects it’s running in a global environment, it checks for a .venv. If that’s missing, it creates the environment, installs your dependencies, and then restarts itself.

import os
import sys
import pathlib
import subprocess
 
def bootstrap():
    """Ensures the script runs inside a local venv, creating it if necessary."""
    root = pathlib.Path(__file__).parent.resolve()
    venv_path = root / ".venv"
    
    # Platform-specific executable path
    if sys.platform == "win32":
        venv_python = venv_path / "Scripts" / "python.exe"
    else:
        venv_python = venv_path / "bin" / "python"
 
    # 1. If we are already in the venv, just return and run the script
    if sys.executable == str(venv_python):
        return
 
    # 2. If venv doesn't exist, create it and install requirements
    if not venv_python.exists():
        print("---> Creating virtual environment...")
        subprocess.run([sys.executable, "-m", "venv", str(venv_path)], check=True)
        
        reqs = root / "requirements.txt"
        if reqs.exists():
            print("---> Installing dependencies...")
            subprocess.run([str(venv_python), "-m", "pip", "install", "-r", str(reqs)], check=True)
 
    # 3. Restart the script using the venv interpreter
    print(f"---> Relaunching via {venv_python}...")
    os.execv(str(venv_python), [str(venv_python)] + sys.argv)
 
# Call this at the absolute top
bootstrap()
 
# --- Normal script starts here ---
import pandas as pd # Example of a heavy lib that needs the venv
print("Successfully running in the isolated environment.")

The “Why”

This pattern creates a “fire and forget” experience for internal tools. It removes the friction of environment management for teammates who might just want to run ./script.py without worrying about their current shell state. It’s particularly powerful for crontab entries or CI/CD runners where you want to guarantee the environment without writing extra boilerplate in your config files.