Your Python script's print() output is useless at 3 a.m. Here is how to switch to logging in 10 lines
You wrote a script, put it on a cron job, and one night it fails. You open the output and find a wall of print() lines with no timestamps, no severity, and no idea which line came from which step. Python's built-in logging module fixes this, and you do not need to install anything. Here is the smallest setup that is actually worth using. Step 1: replace print with a configured logger import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) log = logging.getLogger(__name__) log.info("starting job") log.warning("config file missing, using defaults") log.error("could not reach the API") Output: 2026-10-25 16:40:02,114 INFO __main__: starting job 2026-10-25 16:40:02,114 WARNING __main__: config file missing, using defaults 2026-10-25 16:40:02,115 ERROR __main__: could not reach the API Every line now has a time, a level, and the module that wrote it. Step 2: log to a file that does not grow forever A plain file handler will keep growing until your disk is full. RotatingFileHandler keeps a few capped files instead: import logging from logging.handlers import RotatingFileHandler handler = RotatingFileHandler("job.log", maxBytes=1_000_000, backupCount=3) handler.setFormatter(logging.Formatter( "%(asctime)s %(levelname)s %(name)s: %(message)s" )) root = logging.getLogger() root.setLevel(logging.INFO) root.addHandler(handler) root.addHandler(logging.StreamHandler()) # keep console output too This gives you job.log, job.log.1, job.log.2 and job.log.3, each up to about 1 MB. Step 3: log exceptions with the traceback This is the one that saves you at 3 a.m. Inside an except block, use log.exception: try: data = fetch_report() except Exception: log.exception("fetch_report failed") raise It logs at ERROR level and appends the full traceback automatically. No manual traceback.format_exc() needed. Step 4: change verbosity without editing code import os logging.getLogger().setLevel(os.environ.get("LOG_LEVEL", "INFO")) Now LOG_LEVEL=DEBUG python job.py gives you detail when you need it, and normal runs stay quiet. Three gotchas basicConfig only works once. If any handler is already configured on the root logger, later calls do nothing (unless you pass force=True). Call it once, at the entry point of your program. Pass values as arguments, not f-strings. Write log.info("got %d rows", n) instead of log.info(f"got {n} rows"). The string is only built if the message is actually emitted. Libraries should not configure logging. If you write a module other people import, only call logging.getLogger(__name__) and let the application decide the handlers. The official reference is worth bookmarking: the Logging HOWTO and the Logging Cookbook in the Python docs cover everything above in more depth, including structured output and multiple processes. Quick recap basicConfig once, with a format that includes time and level RotatingFileHandler so logs do not eat your disk log.exception inside except for free tracebacks an environment variable for the log level What is the worst thing you have ever had to debug with nothing but print() output, and what finally made you switch?
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to