Logging
Replace print() with Python’s logging module — timestamped messages, severity levels, file output, and production-ready configuration.
"print() is for exploring. logging is for running. The difference: logging tells you when something happened, where in the code, and how severe it was — print tells you nothing."
Why Switch from print() to logging?
No severity — is this an error or info?
Can't be silenced in production
Goes to console only
Can't filter by importance
No module name shown
5 severity levels (DEBUG to CRITICAL)
Set level to silence debug in prod
Write to console AND file simultaneously
Filter messages by module or level
Shows which file + function logged it
The 5 Logging Levels — Know Them Cold
DEBUG
10
Detailed internal info. "Loading config from /etc/app.conf". Development only.
INFO
20
Normal events. "Server started", "User logged in", "Order #42 processed".
WARNING
30
Something unexpected, but the app keeps running. "Disk 85% full".
ERROR
40
A real problem — a function failed, data is missing, API call failed.
CRITICAL
50
App-level failure. "Database unreachable", "Out of disk space". Needs immediate attention.
level=logging.WARNING means only WARNING, ERROR, and CRITICAL messages are shown — DEBUG and INFO are silenced. This is how you keep production logs clean.
Basic Setup — Configure Once at Start
import logging
# Call this ONCE at the top of your main script
logging.basicConfig(
level = logging.DEBUG, # show everything
format = "%(asctime)s %(levelname)-8s %(message)s",
datefmt= "%Y-%m-%d %H:%M:%S",
)
# Now log from anywhere in your code
logging.debug("Loading config file...")
logging.info("Server started on port 8000")
logging.warning("Memory usage above 80%")
logging.error("Failed to connect to database")
logging.critical("Disk full — cannot write logs!")
2024-06-01 14:22:01 DEBUG Loading config file...
2024-06-01 14:22:01 INFO Server started on port 8000
2024-06-01 14:22:01 WARNING Memory usage above 80%
2024-06-01 14:22:01 ERROR Failed to connect to database
2024-06-01 14:22:01 CRITICAL Disk full — cannot write logs!
Named Loggers — Best Practice for Every Module
Instead of using the root logger, create a named logger per module. This lets you see which file a message came from, and control log levels per module independently:
import logging
# __name__ is the module name, e.g. "orders"
# Do this at the TOP of every module you write
logger = logging.getLogger(__name__)
def process_order(order_id, amount):
logger.info(f"Processing order {order_id} for ₹{amount}")
try:
if amount <= 0:
raise ValueError(f"Invalid amount: {amount}")
logger.debug(f"Order {order_id} validated OK")
logger.info(f"Order {order_id} completed successfully")
return True
except Exception as e:
logger.error(f"Order {order_id} failed: {e}", exc_info=True)
# exc_info=True → also logs the full stack trace
return False
Log to File AND Console at the Same Time
basicConfig goes to one place. For both console and file, use handlers:
import logging
logger = logging.getLogger("myapp")
logger.level = logging.DEBUG
# Handler 1: console — show INFO and above
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
# Handler 2: file — save DEBUG and above (full detail)
file_h = logging.FileHandler("app.log")
file_h.setLevel(logging.DEBUG)
file_h.setFormatter(logging.Formatter(
"%(asctime)s %(name)s %(levelname)s %(message)s"
))
logger.addHandler(console)
logger.addHandler(file_h)
logger.debug("Verbose info — file only")
logger.info("App started — console AND file")
logger.error("Something went wrong!")
Real Example — Request Logger for a Flask App
import logging, time
from flask import Flask, request, g
logging.basicConfig(
filename="requests.log", level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger("requests")
app = Flask(__name__)
@app.before_request
def start_timer():
g.start = time.time()
@app.after_request
def log_request(response):
duration = (time.time() - g.start) * 1000
logger.info(
f"{request.method} {request.path} "
f"{response.status_code} {duration:.1f}ms"
)
return response
# Every request now logs: GET /api/books 200 3.2ms
In your production config set level=logging.WARNING. This hides all the DEBUG and INFO noise, so only actual problems appear in your logs. Keep DEBUG level only for local development where you need the full detail.
"In a long-running app, logs are your flight recorder. If something crashes at 3am, good logs tell you exactly what was happening in the 5 minutes before impact."
— Shurai🧠 Quiz — Q1
What is the key advantage of logging over print()?
🧠 Quiz — Q2
You set level=logging.WARNING. Which messages will appear?
🧠 Quiz — Q3
What does logger = logging.getLogger(__name__) do?
🧠 Quiz — Q4
You want to log to both the console (INFO level) and a file (DEBUG level) simultaneously. What do you need to use?