Python "shutil.which() returned None" - Missing External Tool in CI
shutil.which("tool") returned None because the external binary your code shells out to isn’t on PATH in CI. The Python code then crashes when it tries to use the missing path - often as a confusing TypeError or a downstream FileNotFoundError.
What this error means
A script that locates a helper binary with shutil.which gets None, then fails - either a RuntimeError you raise, or a TypeError: expected str ... not NoneType when the None is passed to subprocess. It works locally where the tool is installed.
tool = shutil.which("ffmpeg") # -> None in CI
subprocess.run([tool, "-i", src])
# TypeError: expected str, bytes or os.PathLike object, not NoneType
# or your own:
RuntimeError: required executable 'ffmpeg' not found on PATHCommon causes
External tool not installed on the runner
The binary (ffmpeg, graphviz/dot, git, pandoc) is present on dev machines but not in the minimal CI image, so which finds nothing.
Installed but not on PATH
The tool exists at a non-standard location not in PATH (e.g. installed to /opt/... without a symlink), so shutil.which still returns None.
How to fix it
Install the external tool in CI
# Debian/Ubuntu - example: ffmpeg
apt-get update && apt-get install -y ffmpeg
python -c "import shutil; print(shutil.which('ffmpeg'))"Add the tool’s directory to PATH
If it’s installed but not discoverable, put its bin directory on PATH for the job.
export PATH="/opt/ffmpeg/bin:$PATH"
# GitHub Actions, persist across steps:
echo "/opt/ffmpeg/bin" >> "$GITHUB_PATH"Fail with a clear message
Guard the lookup so a missing tool produces an actionable error instead of a cryptic NoneType crash.
tool = shutil.which("ffmpeg")
if tool is None:
raise RuntimeError("ffmpeg not found on PATH; install it in CI")How to prevent it
- Bake required external binaries into the runner image.
- Validate tool presence early with a clear error, not a NoneType crash.
- Document the OS-level tools your scripts shell out to.