Logging instead of print
The logging module gives every message a level, a timestamp and a destination you can change without editing code. It is the grown-up replacement for debug print() calls.
Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?
print() is perfect for a quick experiment. In a script that runs every night or a program other people use, it falls short: you cannot turn the noisy messages off, you do not know when something happened, and errors get mixed into normal output. The standard library’s logging module fixes all three.
The core idea: levels
Every log message has a level. You pick a threshold once, and messages below it are dropped.
DEBUG: details you only want while hunting a bug.INFO: normal progress (“Loaded 120 orders”).WARNING: something unexpected, but the program continues. This is the default threshold.ERROR: an operation failed.CRITICAL: the program probably cannot continue.
A minimal, correct setup
import logging
logger = logging.getLogger(__name__)
def load_orders(path):
logger.info("Loading orders from %s", path)
try:
with open(path, encoding="utf-8") as f:
lines = f.read().splitlines()
except FileNotFoundError:
logger.exception("Could not read %s", path)
return []
logger.debug("Read %d lines", len(lines))
if not lines:
logger.warning("%s is empty", path)
return lines
def main():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
load_orders("orders.txt")
if __name__ == "__main__":
main()
Output looks like 09:07:05 INFO __main__: Loading orders from orders.txt. Change level=logging.INFO to logging.DEBUG and the debug line appears too, with no other edits.
Four habits that matter
- One logger per module:
logger = logging.getLogger(__name__)at the top. The logger name then tells you which file produced each line. - Configure once, at the entry point: call
logging.basicConfig()only inmain()or theif __name__ == "__main__"block. Library modules should never configure logging; they only create loggers and log. - Pass values as arguments: write
logger.info("Saved %s", path), not an f-string. The text is only formatted if the message is actually emitted, and tools that group logs can recognize identical messages. - Use
logger.exception()insideexcept: it logs at ERROR level and appends the full traceback automatically.
Sending logs to a file and the console
basicConfig covers most scripts; it also accepts filename="app.log". When you want different destinations with different thresholds, attach handlers yourself:
import logging
import sys
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
console = logging.StreamHandler(sys.stderr)
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logfile = logging.FileHandler("myapp.log", encoding="utf-8")
logfile.setLevel(logging.DEBUG)
logfile.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
logger.addHandler(console)
logger.addHandler(logfile)
logger.debug("only in the file")
logger.info("in both places")
logging.getLogger("myapp.db").warning("child loggers propagate to myapp")
Logger names form a tree split on dots. myapp.db is a child of myapp, so its messages travel up to the parent’s handlers. That is why naming loggers with __name__ (which follows your package layout) works so well.
Where print() still belongs
Use print() for the program’s actual output: the report a CLI tool produces, or JSON another program will read. Use logging for messages about what the program is doing. Logging writes to standard error by default, so python tool.py > result.json keeps the data clean while progress messages still appear in the terminal.
For long-running services, look at logging.handlers.RotatingFileHandler so log files do not grow forever, or log to standard error and let your container platform or systemd collect it.
Did this work for you?
No votes yet. Be the first.
If it did not work, the report form below tells the editors what went wrong.
See also
- 0003 pathlib cheat sheet: files and folders in Python pathlib.Path is the modern way to build paths, read and write files, and list or search folders in Python. This page lists… Beginner · Cheat sheet · Any system Article Files & automation 15 Oct 2026
- 0009 CSV to JSON converter A small command-line tool that turns a CSV file into a JSON array of objects, with optional number detection, custom delimiters and… Beginner · Step by step · Any system Script CSV & JSON · Files & automation 15 Oct 2026
- 0010 Find duplicate files by content Scans folders and lists files with identical content, using file size first and SHA-256 hashes second. It is read-only and only reports… Intermediate · Step by step · Any system Script Files & automation 15 Oct 2026
- 0074 pandas basics: read a CSV, filter rows and summarize with groupby Load a CSV into a pandas DataFrame, inspect it, filter rows with conditions, and compute totals per group, with the SQL equivalent… Intermediate · Step by step · Any system Article Data analysis · Files & automation 15 Oct 2026
- 0075 CSV to JSON converter (Python, standard library) Convert any CSV file with a header row into a JSON array or JSON Lines. Handles quoted fields, a byte order mark,… Beginner · Step by step · Any system Script CSV & JSON · Files & automation 15 Oct 2026
- 0084 Tiny JSON echo server for testing HTTP clients A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status… Intermediate · Step by step · Any system Script Files & automation · Making requests 15 Oct 2026
Repos in the wild
- Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- Dominik +4,036 chars