Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A Python program that succeeds in your terminal can fail in crontab because cron does not reproduce your interactive shell session. It starts a non-interactive process with its own shell, environment variables, working directory, permissions and command parsing. Your terminal may have selected a virtual environment, loaded settings from startup files, exported credentials and changed into the project directory; cron may have none of those conditions.

The dependable fix is to make every dependency explicit: call the intended Python interpreter by absolute path, use absolute or script-relative file paths, define required environment variables, account for /bin/sh syntax (including literal percent signs), run as a user with the needed permissions, and send output and errors to an absolute log file. The sections below show how to diagnose each difference and build a job that can be reproduced outside cron.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What changes between a terminal run and a cron run?

When you type python job.py, your interactive shell has already made many decisions. It may have read .profile or .bashrc, prepended a virtual environment to PATH, loaded pyenv or conda settings, exported API keys, and left you in the project directory. Cron starts the command independently, so none of those terminal-side effects can be assumed.

On Linux, a crontab command is run through /bin/sh by default. Linux crontab documentation states that LOGNAME and HOME come from the crontab owner’s account, while SHELL is /bin/sh. POSIX also requires a default environment containing HOME, LOGNAME, PATH and SHELL, but that does not mean your personal additions are present. Unredirected output or errors may be mailed to the user, which is easy to miss if local mail is not monitored.

Cron also parses the command before the shell executes it. A percent sign (%) in a crontab command is converted to a newline; the remaining text is supplied on standard input. A date format such as date +%Y-%m-%d can therefore break an otherwise valid command unless the percent signs are escaped as % or the logic is moved into a script file.

First checks: confirm the job, account and schedule

Inspect the installed crontab

  1. Run crontab -l as the account that should execute the job. A system crontab in /etc/cron.d has a different format because it includes a user field.
  2. Ensure the entry is saved and that the crontab file ends with a newline. Some cron implementations ignore a final line without one.
  3. Temporarily use a frequent schedule, such as * * * * *, while diagnosing. Restore the real schedule after the command is verified.
  4. Check the machine’s cron service and system logs using the logging tools provided by your distribution. A missing service, disabled timer, or rejected crontab is a scheduling problem rather than a Python problem.

Verify which user runs it

The account owning a user crontab determines its HOME, LOGNAME, file permissions and access to credentials. A script that can read a project in your home directory may fail when installed for a service account. Test every parent directory as well as the file itself: the cron user needs execute permission on directories and read or write permission where the program needs it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make Python interpreter selection deterministic

The command python is resolved through PATH. Your terminal and cron can resolve it to different interpreters, or cron may not find it at all. Different interpreters commonly have different installed packages and Python versions.

Call the virtual-environment executable directly

Use the executable inside the environment instead of relying on activation:

/srv/app/.venv/bin/python /srv/app/job.py

Python’s virtual-environment documentation explains that activation prepends the environment’s bin directory to PATH; activation is optional when you invoke that interpreter directly. This removes dependence on shell startup files and ensures imports come from the environment you selected.

Find the exact path while the environment is active with command -v python, then verify it:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
/srv/app/.venv/bin/python -c 'import sys; print(sys.executable); print(sys.version)'

Run the same absolute command manually before putting it in cron. If it fails there, fix the environment or dependencies first.

Use a shebang only when its path is stable

A script beginning with #!/usr/bin/env python still depends on PATH. For a cron job, either execute the absolute environment interpreter explicitly or use a shebang with a fixed interpreter path and make the file executable. The explicit crontab command is easier to inspect during an incident.

Eliminate working-directory surprises

A relative path such as data/input.csv is interpreted against the process’s current working directory, not against the directory containing your Python file. The directory from which you launched the terminal is therefore not a reliable assumption in cron.

Build paths from the source file

For a job whose data lives beside the application, derive paths with pathlib:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
input_file = BASE_DIR / "data" / "input.csv"
output_file = BASE_DIR / "output" / "result.json"

Create output directories deliberately and fail with a clear message if an input is absent. This approach remains stable even when cron starts the process from another directory.

Change directory in a wrapper when the application expects it

Some tools load configuration relative to the current directory. Put that requirement in a small wrapper rather than hiding it in a long crontab line:

#!/bin/sh
set -eu
cd /srv/app
exec /srv/app/.venv/bin/python /srv/app/job.py

Save it, make it executable with chmod 750 /srv/app/run-job.sh, and schedule the wrapper by absolute path. exec preserves the useful exit status and makes the Python process visible to process supervisors.

Declare the environment your program needs

Variables exported in a terminal session, loaded by .bashrc, supplied by pyenv or conda, or injected by a desktop secret manager may not exist in cron. Treat required variables as part of the program’s contract.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Set safe, non-secret defaults in the crontab

A predictable baseline can be declared at the top of the crontab:

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * /srv/app/.venv/bin/python /srv/app/job.py >> /srv/app/logs/job.log 2>&1

The paths must exist for the cron account. Keep the PATH list limited to trusted system directories and any explicitly required application directory.

Handle secrets without exposing them

Putting a password or API token directly in a crontab can expose it to anyone who can read that crontab, process diagnostics or backups. Prefer a root-owned environment file with restrictive permissions, an operating-system secret facility, or a wrapper that reads a protected file. Validate that the cron user can read the chosen secret source, and never print secret values in diagnostic logs.

If you do use an environment file, load it with a shell mechanism compatible with /bin/sh and quote values carefully. An application can also fail because a variable is present but empty; check required names at startup and exit with a specific error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Respect cron’s shell and percent rules

Crontab commands are not automatically interpreted by Bash. Bash-only constructs such as arrays, [[ ... ]], process substitution and some brace expansions can fail under /bin/sh. Put complex logic in an executable script with an explicit shebang, or set SHELL=/bin/bash only when Bash is installed and genuinely required.

Escape literal percent signs

This entry is unsafe because cron treats each unescaped percent sign specially:

0 1 * * * /srv/app/.venv/bin/python /srv/app/report.py --stamp $(date +%Y-%m-%d)

Escape the signs in the crontab itself:

0 1 * * * /srv/app/.venv/bin/python /srv/app/report.py --stamp $(date +%Y-%m-%d)

Alternatively, calculate the timestamp inside Python or place the command in a wrapper script, where normal shell percent behavior applies. Also review quoting, redirection order and command substitutions when copying a line from an interactive shell.

Make failures visible with logging

Terminal output disappears when no terminal is attached. Redirect both streams to a known absolute path:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
30 2 * * * /srv/app/.venv/bin/python /srv/app/job.py >> /srv/app/logs/job.log 2>&1

Create the log directory first and ensure the cron user can write it. Rotating the log prevents an unattended job from filling the filesystem.

Log the facts that distinguish cron from a terminal

Add a temporary startup diagnostic (or a structured permanent one) that records safe values:

import os
import sys

print({
    "executable": sys.executable,
    "version": sys.version,
    "cwd": os.getcwd(),
    "argv": sys.argv,
    "home": os.environ.get("HOME"),
    "path": os.environ.get("PATH"),
})

Do not include tokens, passwords or complete environment dumps. Compare this output with a successful terminal run. Differences in executable, current directory, arguments or selected variables usually identify the fault quickly.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A repeatable diagnostic sequence

  1. Confirm scheduling: inspect crontab -l, verify the cron service, and check system cron logs.
  2. Pin both paths: replace python and the script name with absolute paths.
  3. Capture startup facts: log sys.executable, sys.version, os.getcwd(), sys.argv and a safe environment subset.
  4. Test permissions: as the cron user, read inputs, traverse every parent directory, create the output file and append to the log location.
  5. Test imports: run the exact absolute command outside cron, then repeat with a minimal environment. For example: env -i HOME="$HOME" PATH="/usr/bin:/bin" /srv/app/.venv/bin/python /srv/app/job.py. Add only variables the program demonstrably needs.
  6. Inspect logs, not the terminal: read the application log and the system cron log after a scheduled attempt.
  7. Audit syntax: check percent signs, shell-specific operators, quoting, redirection and line endings.

The minimal-environment test is not a replacement for cron: it still runs from your shell and under your account. Its value is that it removes inherited variables and PATH entries, making hidden assumptions reproducible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose between a direct line and a wrapper

Approach Interpreter determinism Path and environment setup Observability and reproduction Best use
Direct crontab command High when an absolute interpreter path is used Good for a short command with explicit variables; fragile with complex quoting Good when stdout and stderr are redirected to an absolute log Small jobs with few setup steps
Shell wrapper High when it calls the absolute interpreter Clear place for set -eu, cd, variable checks and setup Easy to run the same file manually; shell syntax is visible and testable Jobs needing directory changes, validation or multiple commands

Both approaches are portable across common cron implementations when they use absolute paths and conservative shell syntax. A wrapper usually wins once the line needs nontrivial quoting or environment preparation; a direct line remains easier to see at a glance.

A production-ready pattern

For a simple application, combine an explicit shell and PATH with the direct virtual-environment interpreter and absolute logging:

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * /srv/app/.venv/bin/python /srv/app/job.py >> /srv/app/logs/job.log 2>&1

If the application requires a project directory, use the wrapper shown earlier and schedule /srv/app/run-job.sh instead. In both cases, test as the actual cron user, keep permissions restrictive, and retain enough startup logging to identify interpreter, directory and argument errors without disclosing secrets.

Common symptoms and their likely causes

  • python: command not found: cron’s PATH does not include the interpreter. Use the absolute virtual-environment executable.
  • ModuleNotFoundError only in cron: the job selected a different Python installation or environment. Log sys.executable and call the intended interpreter directly.
  • FileNotFoundError for a file that exists: a relative path was resolved from another current directory. Use Path(__file__).resolve().parent, an absolute path or an explicit cd.
  • Credentials or configuration are missing: variables loaded by shell startup files are absent. Define non-secret settings explicitly and provide secrets through a protected mechanism.
  • The command stops at a date format or receives strange input: an unescaped percent sign was converted by cron. Escape it or move the command into a script.
  • The job appears silent: output was not redirected, or mail delivery is not monitored. Redirect both streams to an absolute log and check cron’s system log.
  • Permission denied: the cron account cannot traverse a parent directory, read an input, execute the interpreter or write the log/output path. Check each permission boundary.

What “fixed” means

You can call the job fixed when the installed crontab, run as its real account, executes the same interpreter and arguments every time; paths do not depend on the launch directory; required variables are declared; shell metacharacters are intentional; permissions are tested; and a dated log records success or a useful failure. A terminal success by itself proves none of those cron conditions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.