Course Progress95%
🍎 Python Professional Python Topic 95 / 100
⏳ 8 min read

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."

— Shurai

Why Switch from print() to logging?

✗ print() — development only
No timestamp — when did this happen?
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
✓ logging — production-ready
Timestamp on every message
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.
💡 Setting 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

python — basicConfig (simplest setup)
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!")
output — every message gets a timestamp and level
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:

python — orders.py
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:

python — handlers: write to console AND file
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

python — log every incoming request
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
Production tip: use WARNING level by default

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?