DEV Community

RoTSL
RoTSL

Posted on • Originally published at Medium on

I built a safety net for python environments because I was tired of debugging “It works on my machine”

Why every python developer needs a preflight check for their code

image

I used to have a recurring nightmare. I’d be halfway through a machine learning experiment, three hours into training, when suddenly everything would explode. Not because my code was wrong, but because my environment was quietly broken in a way I couldn’t see coming. Wrong Python version. A package that got installed with pip instead of conda. A wheel that claimed to support my architecture but didn’t. Rosetta 2 running my arm64 Python as x86_64 and tanking my GPU acceleration.

The error messages were always cryptic. The fixes were always tedious. And the worst part? I never knew if my environment was actually healthy until something went wrong.

So I built EnvGuard—a CLI tool that validates your Python environment before you run your code, not after it breaks. Think of it as a preflight checklist for your Python projects. If something’s wrong, it blocks execution and tells you exactly what’s broken and how to fix it. If everything passes, your command runs in a validated environment.

It’s macOS-first (because that’s where I do my work), runs on Linux with partial support, and deliberately doesn’t support Windows (because life’s too short for three-platform maintenance). You can install it from PyPI as envguard-tool — the CLI command is just envguard.

The Problem: Python Environments Are Fragile and Invisible

Python environment management is a solved problem in the same way that herding cats is a solved problem. Technically there are tools. Practically, things go wrong constantly.

Here’s what actually happens in the wild:

The architecture confusion. You install Python on an M1 Mac, but somehow you’re running the x86\_64 version under Rosetta 2. Everything works, but your Metal Performance Shaders (MPS) acceleration is silently disabled. PyTorch falls back to CPU. Your training takes 10x longer. You don’t notice until you check activity monitor and see no GPU usage.

The mixed ownership trap. You create a conda environment, but then you pip install something because the conda version is outdated. Then you conda install something else. Now you have packages owned by two different managers, and pip check is screaming about conflicts, but your code still runs so you ignore it until it doesn’t.

The CUDA delusion. You’re on macOS, but your requirements.txt includes torch==2.0.0+cu118 because you copied it from a Linux server. It installs fine. It even imports. But CUDA doesn’t exist on Apple Silicon, and your code fails three layers deep in a stack trace that mentions nothing about GPU compatibility.

The “it worked yesterday” mystery. Your environment was fine. Then you updated one package. Now something else is broken. You have no idea what changed or when.

These aren’t exotic edge cases. They’re daily experiences for Python developers working on ML, data science, or scientific computing projects. The existing tools — conda, pip, poetry, uv, pyenv — are great at creating environments. They’re terrible at validating them continuously.

The Solution: Preflight Validation Every Single Time

EnvGuard’s core idea is simple: instead of running python train.py and hoping, you run envguard run — python train.py. Before your code executes, EnvGuard runs a nine-step preflight pipeline:

  1. Detect the host  — OS version, architecture (native arm64 vs Intel vs Rosetta 2), available package managers, network connectivity
  2. Discover the project  — scan for pyproject.toml, requirements.txt, environment.yml, Pipfile, poetry.lock, etc.
  3. Analyze intent  — figure out what environment type (venv/conda/pipenv/poetry), Python version, and accelerator targets (CPU/MPS/CUDA) the project needs
  4. Evaluate rules  — run 15+ validation rules to catch problems
  5. Fail-fast on critical issues  — block execution if anything is unrecoverable
  6. Create a resolution plan  — determine exactly how to satisfy the environment requirements
  7. Create or repair the environment — make sure the actual environment matches the requirements
  8. Validate the environment  — run pip check or equivalent to verify consistency
  9. Smoke test — try importing key packages in an isolated subprocess to catch runtime failures

If any step fails with a CRITICAL finding, your command never runs. You get a clear error message explaining what went wrong and how to fix it. No cryptic tracebacks. No debugging environment issues at 2am.

What EnvGuard Actually Catches

The rules engine evaluates 15+ specific checks. Here are the ones that have saved me the most pain:

Rule Severity What it catches
CUDA_ON_MACOS CRITICAL Any CUDA dependency on macOS (hardware impossibility)
ROSETTA_TRANSLATION_DETECTED WARNING x86_64 Python running under Rosetta 2 on Apple Silicon (kills MPS performance)
ARCHITECTURE_MISMATCH ERROR Python architecture doesn't match project requirements
MIXED_PIP_CONDA_OWNERSHIP WARNING Packages installed by both pip and conda (dependency hell indicator)
WHEEL_INCOMPATIBLE WARNING Wheel file doesn't match current platform/architecture
BROKEN_ENVIRONMENT ERROR Active venv/conda is missing Python binary or critical files
PYTHON_VERSION_BELOW_MINIMUM ERROR Python version below requires-python in pyproject.toml
MPS_NOT_AVAILABLE INFO Apple Silicon present but MPS not available (usually means PyTorch wasn't built with MPS support)

The CUDA_ON_MACOS rule alone has probably saved me hours of debugging. Here’s what happens: you copy a requirements.txt from a Linux machine that specifies torch==2.1.0+cu118. You install it on your M1 Mac. It seems to work — pip doesn’t complain, the import succeeds. But when you actually try to move tensors to the GPU, you get a cryptic error about CUDA devices not being available. EnvGuard catches this at the dependency resolution stage and blocks execution with a message telling you to use mps or cpu targets instead.

The ROSETTA_TRANSLATION_DETECTED rule is subtler but equally important. If you’re running an x86_64 Python binary on an Apple Silicon Mac (usually because you installed it before Rosetta was properly configured, or you’re using an old pyenv), everything works — but MPS acceleration is silently disabled. Your ML training runs on CPU. Your inference is 10x slower than it should be. EnvGuard detects this via sysctl proc_translated and warns you that you’re leaving performance on the table.

The Technical Architecture: How It Actually Works

EnvGuard is built as a layered Python package with clear separation of concerns. The source is in the GitHub repo under src/envguard/.

CLI Layer (cli.py): Typer-based interface with 25 commands across environment management, dependency resolution, lock files, publishing, and self-updating. Every command supports  — json output for CI/CD integration.

Orchestration Layer (preflight.py, doctor.py): The preflight engine runs the nine-step pipeline. The doctor runs standalone diagnostics without execution. Both use the same underlying detection and rules systems.

Domain Layer : The heavy lifting happens here:

  • detect.py — HostDetector class gathers OS, architecture, Python, shell, network, and permission facts
  • rules.py — RulesEngine evaluates all 15+ validation rules
  • repair.py — RepairEngine can automatically fix broken environments (recreate venvs, fix mixed ownership, switch Python versions)
  • models.py — Pydantic models for HostFacts, ProjectIntent, RuleFinding, ResolutionRecord, etc.
  • project/ — Discovery (scanning for project files), intent analysis (inferring requirements), resolution (dependency solving), and lifecycle management
  • resolver/ — Pluggable backends for PyPI (BFS resolution via JSON API), uv, pip, and conda
  • lock/ — Lock file generation and management (envguard.lock in TOML format with SHA-256 content hashes)
  • update/ — Self-updating mechanism with SHA-256 verification and rollback support

Platform Layer : macOS-specific code for permissions, Rosetta detection, Xcode CLI tools, and LaunchAgent management. Linux gets partial support here — core pipeline works, but no LaunchAgent, no MPS detection, no Rosetta checks.

All state files (in .envguard/) are written atomically using write-to-temp-then-rename to prevent corruption from interrupted writes. Every subprocess call has explicit timeouts. The security model is documented in detail — checksum verification for updates, no shell=True with string interpolation, path traversal protection for archive extraction.

Real Usage: What My Workflow Looks Like

I work on a lot of ML projects with different requirements. Some need PyTorch with MPS. Some need TensorFlow (which has its own special hell on macOS). Some are pure Python data pipelines. Here’s how I actually use EnvGuard day-to-day.

Starting a new project:

cd ~/projects/new-ml-experiment
envguard init
Enter fullscreen mode Exit fullscreen mode

This creates a .envguard/ directory with state.json, envguard.toml (config), and subdirectories for snapshots, cache, logs, and backups. It scans my project files to figure out what I’m building.

Checking if everything is healthy:

envguard doctor
Enter fullscreen mode Exit fullscreen mode

This runs 10 diagnostic checks: host detection, project discovery, Python environment, package manager health, dependency consistency, accelerator support, permissions, network connectivity, and environment ownership. It outputs a report showing what’s working and what’s not.

Running my actual code:

envguard run - python train.py - epochs 100 - batch-size 32
Enter fullscreen mode Exit fullscreen mode

Before train.py executes, the preflight pipeline runs. If my environment has drifted — say, I updated PyTorch and now there’s a version conflict with torchvision — EnvGuard catches it and blocks execution. I can then run envguard repair to fix it automatically, or envguard lock sync to reinstall from my lock file.

Locking dependencies for reproducibility:

envguard resolve
envguard lock generate
Enter fullscreen mode Exit fullscreen mode

resolve uses the PyPI JSON API to resolve my project dependencies to exact pinned versions. lock generate writes an envguard.lock file with SHA-256 content hashes. I commit this to git. When someone else clones the repo, they run envguard install — from-lock and get exactly the same environment I have.

Self-updating:

envguard update - dry-run # check if there's a new version
envguard update # actually update with SHA-256 verification and automatic rollback snapshot
Enter fullscreen mode Exit fullscreen mode

EnvGuard can update itself. Before applying an update, it creates a rollback snapshot. If something goes wrong, envguard rollback restores the previous version.

The Lock File: Reproducibility Without the Pain

Python dependency management has a reproducibility problem. requirements.txt with loose version constraints means “install something that hopefully works.” requirements.txt with pinned versions means “this worked on my machine at one specific moment, but good luck if you’re on a different architecture or Python version.”

EnvGuard’s lock file (envguard.lock) tries to be smarter. It’s a TOML file that includes:

  • The exact resolved dependency graph with specific versions
  • SHA-256 content hashes for verification
  • Platform and Python version markers (so you can have different resolutions for macOS-arm64 vs Linux-x86_64 if needed)
  • The source files that contributed to the resolution (pyproject.toml, requirements.txt, etc.)

The lock file is human-readable but machine-generated. You don’t edit it manually. You regenerate it with envguard lock generate or update specific packages with envguard lock update — package <name>.

In CI, you can run envguard lock check to verify that the lock file is up-to-date with your source requirements. It exits with code 13 if stale, which you can use to fail builds that might have inconsistent dependencies.

What It Doesn’t Do (And Why)

EnvGuard has deliberate limitations that are worth understanding:

It doesn’t intercept unmanaged launches. If you run python train.py directly, EnvGuard doesn’t see it. Only commands routed through envguard run get validated. This is by design — EnvGuard is opt-in, not a system-wide interceptor that could break other workflows.

It doesn’t support Windows. The codebase uses POSIX-specific APIs throughout (os.access() for permissions, list-form subprocess arguments, /tmp paths). Adding Windows support would require a parallel implementation of the platform layer, and I don’t use Windows enough to maintain that. WSL2 works if you need it.

It doesn’t make CUDA work on macOS. Apple Silicon physically cannot run NVIDIA CUDA. EnvGuard detects CUDA dependencies and blocks them with a clear error, but it can’t magically add CUDA support where none exists.

It doesn’t auto-activate environments on directory change. If you want cd my-project to automatically activate the right venv, use direnv. EnvGuard’s shell hooks are minimal and opt-in — they just load the integration, not the environments themselves.

These limitations are documented in the repo’s docs/limitations.md and tracked as architectural decisions in docs/adrs/.

Installation and Getting Started

If you’re on macOS 12+ (Monterey) or Linux, installation is straightforward:

pip install envguard-tool
Enter fullscreen mode Exit fullscreen mode

The PyPI package is named envguard-tool because envguard was taken. The CLI command and Python import are both just envguard.

For macOS, there’s also a bootstrap script that installs shell hooks and the LaunchAgent for automatic update checking:

git clone https://github.com/rotsl/envguard.git
cd envguard
bash scripts/bootstrap.sh
Enter fullscreen mode Exit fullscreen mode

Once installed, verify it works:

envguard - version
envguard doctor
Enter fullscreen mode Exit fullscreen mode

Then initialize any Python project:

cd /path/to/your/project
envguard init
envguard run - python your_script.py
Enter fullscreen mode Exit fullscreen mode

Why I Built This (And Who It’s For)

I built EnvGuard for myself, primarily. I’m a researcher working on machine learning for biology — specifically computer vision for fungal pathogen analysis. I work on Apple Silicon Macs. I collaborate with people on Linux servers. I deal with PyTorch, TensorFlow, JAX, and a lot of scientific Python packages with complex native dependencies.

I was tired of debugging environment issues that had nothing to do with my actual research. I wanted a tool that would catch problems before they cost me hours of training time or corrupted experimental results.

EnvGuard is for Python developers who:

  • Work on macOS (especially Apple Silicon) and are tired of Rosetta/architecture surprises
  • Need MPS acceleration for PyTorch and want to know when it’s not actually available
  • Collaborate across different machines and need reproducible environments
  • Are tired of “works on my machine” and want validation that happens before execution
  • Prefer CLI tools that integrate into existing workflows rather than replacing them entirely

It’s not for everyone. If you’re a web developer working with simple Python environments, you probably don’t need this. If you’re on Windows, this won’t help you (yet). If you want a full IDE-like environment manager with GUI buttons, look elsewhere.

But if you’re doing scientific Python or ML and you’ve ever lost a day to a broken environment that you didn’t know was broken until it was too late — EnvGuard might save you some pain.

Python environment management has been broken for a long time. We’ve accepted “it works on my machine” as an inevitable part of the development experience. We’ve normalized spending hours debugging issues that have nothing to do with our actual code.

I don’t think it has to be this way. EnvGuard is my attempt to bring some of the safety and validation we expect from production systems (preflight checks, reproducible builds, clear error messages) to the messy world of Python development.

It’s not perfect. It’s alpha software with known limitations. But it’s already saved me hours of debugging, and I hope it can do the same for you.

If you’re tired of environment surprises, give it a try. Run pip install envguard-tool, run envguard doctor on your project, and see what it finds. You might discover that your “working” environment has been quietly broken in ways you never noticed.

Links:

Top comments (0)

The discussion has been locked. New comments can't be added.