A script that works when you run it yourself, in front of the screen, is only halfway there. When it runs on its own at six in the morning, nobody sees the error messages, nobody notices that a file was missing, nobody realises it processed zero rows instead of three hundred. The difference between a test script and an automation you can count on lies in a few precautions: an activity log, explicit error handling and a way to alert someone when something goes wrong.
Replacing print with logging
The standard library's logging module does what print does, but with three advantages: it writes to a file, adds the date and time automatically, and distinguishes severity levels.
import logging
logging.basicConfig(
filename="C:/Automation/log/invoices.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
)
log = logging.getLogger("invoices")
log.info("Starting processing")
log.warning("File without a date in its name: %s", "scan.pdf")
log.error("Unable to open %s", "order_15.pdf")
Each line of the file will look like 2025-03-10 06:00:02,315 INFO Starting processing. When a colleague reports that a document wasn't filed, you just search the log for the file name to know whether it was seen, when and with what outcome.
The main levels, from least to most severe, are DEBUG, INFO, WARNING, ERROR and CRITICAL. With level=logging.INFO, debug messages are ignored: you can leave them in the code and switch them back on when you need to investigate.
Stopping the log from growing forever
A script that runs every day for years produces a huge file. The logging.handlers module offers RotatingFileHandler, which starts a new file once a certain size is exceeded, and TimedRotatingFileHandler, which does so at time intervals, keeping only a set number of previous copies.
Handling exceptions without hiding them
When an error isn't handled, Python stops the script and prints the message to the terminal, which nobody reads in a scheduled run. The opposite mistake is just as dangerous: a try with an empty except that swallows every problem and makes it look as if all went well.
The right path lies in between. You handle errors for a single item without stopping work on the others, and you log them:
errors = 0
for file in file_list:
try:
process(file)
log.info("Processed %s", file.name)
except Exception:
errors += 1
log.exception("Error on %s", file.name)
log.exception records the message together with the full stack trace, that is, the list of code lines that led to the error. It is the information you need to understand the problem days later.
Exit codes
A script communicates its outcome to the operating system with an exit code: zero if all went well, a different value in case of problems. Windows Task Scheduler and cron record it, and other programs can rely on it.
import sys
log.info("Done: %d files, %d errors", len(file_list), errors)
sys.exit(1 if errors else 0)
Without this line the script always ends with zero, even when half the files failed, and Task Scheduler will show a reassuring 0x0.
Alerting someone
The log is valuable, but it has to be read. Errors need an active notification. The most common options:
| Method | Advantages | Limits |
|---|---|---|
| Simple, reaches you anywhere | May get ignored among all the others | |
| Message in a work chat | Visible to the team | Requires setting up an integration |
| Status file in a shared folder | No external service | Someone has to check it |
A good rule is to notify only what requires action. An email for every successful run is soon ignored, and with it the one reporting a problem. In my work I send a notification only on error, plus a short weekly summary confirming that everything worked.
Common-sense checks
Some problems are not technical errors and raise no exceptions. The script runs without trouble, but the result is wrong. A few explicit checks catch them:
- zero items: if the input folder is empty on a day when it shouldn't be, that is an anomaly to report;
- out-of-range values: a total ten times higher than the average deserves a warning;
- output file: check that the file produced exists and isn't empty;
- lock against overlapping runs: a lock file created at start-up and removed at the end prevents two instances of the same script from working on the same files at once.
if not file_list:
log.warning("No files to process")
Configuration separate from code
Paths, recipients' email addresses and alert thresholds change more often than the logic. Keeping them in a configuration file, for example in TOML format read with the tomllib module available since Python 3.11, avoids editing the code for every small change and reduces the risk of introducing errors.
In short
- Use
loggingwith date, time and levels instead ofprint, and rotate log files. - Handle errors per item and record them with
log.exception. - Return a non-zero exit code when something fails.
- Notify only what needs action, plus a periodic summary.
- Add checks for empty or anomalous results and prevent overlapping runs.