Python Log Levels Explained: When to Use Each
Choosing the right log level for each message is the first discipline of good observability. Too much logging fills disk space and confuses operators; too little and you cannot diagnose failures. The five Python log levels form a contract between developer and operator: developers promise to use levels correctly, and operators promise to set thresholds that match their alerting and capacity constraints. This article teaches you the intent behind each level and the decision rules that tell you which level to use for any given message.
Log levels are numeric thresholds: DEBUG is 10, INFO is 20, WARNING is 30, ERROR is 40, CRITICAL is 50. When you set a logger to level 30 (WARNING), you see WARNING, ERROR, and CRITICAL messages, but DEBUG and INFO are filtered out. The hierarchy is absolute. Understanding the boundary between adjacent levels—especially between INFO and WARNING—takes practice but is non-negotiable for production readiness.
What Is the Difference Between DEBUG and INFO?
DEBUG is for information that is useful only to developers during troubleshooting. INFO is for information that an operator or business analyst would want to know about normal system operation. The key question: "Would a customer care if I put this in a log file they could download?"
DEBUG messages include:
- Variable values and data structure contents
- Loop iterations and internal control flow
- Execution time measurements for specific functions
- Parameter validation results
import logging
logger = logging.getLogger(__name__)
def calculate_total(items):
logger.debug(f"Input items: {items}") # Detailed diagnostic
subtotal = sum(item['price'] * item['quantity'] for item in items)
logger.debug(f"Subtotal before tax: {subtotal}")
tax_rate = 0.08
total = subtotal * (1 + tax_rate)
logger.info(f"Order total calculated: ${total:.2f}") # Customer-relevant fact
return total
INFO messages include:
- Application startup and shutdown
- Successful completion of user-initiated tasks
- Configuration changes
- Authentication events
- State transitions
The rule: if an operator needs to know about it to understand what the system is currently doing, log it as INFO. If only a developer debugging a specific problem would care, use DEBUG.
When Should You Use WARNING Versus ERROR?
WARNING indicates that something unexpected happened, but the system recovered or chose an alternate path. ERROR indicates that an operation failed and the system could not complete the requested task. This distinction matters for alerting: a WARNING might warrant an email to the on-call engineer; an ERROR warrants a page.
WARNING examples:
- Retry logic triggered (network timeout, but retrying)
- Graceful fallback used (cache miss, falling back to database)
- Deprecated API called (still works, but will break in a future version)
- Resource threshold approached (memory at 80%, but not out)
- Timeout with automatic recovery
import logging
logger = logging.getLogger(__name__)
def fetch_with_retry(url):
for attempt in range(3):
try:
return requests.get(url, timeout=5)
except requests.ConnectTimeout:
if attempt < 2:
logger.warning(f"Request timeout on attempt {attempt + 1}, retrying")
time.sleep(2 ** attempt)
else:
logger.error(f"Request failed after 3 attempts to {url}")
return None
ERROR examples:
- Database query returned no results when one was required
- External API returned a 5xx status code (cannot retry)
- File cannot be written to disk (permissions or space)
- Authentication credentials are invalid or expired
- Data validation failed and operation cannot proceed
The distinction is: if the system automatically recovered (like a retry that succeeded), log it as WARNING. If the task failed and a human operator might need to intervene, log it as ERROR.
What Should Be CRITICAL Versus ERROR?
CRITICAL is for conditions that make the entire application unusable. If you log CRITICAL, the operator should expect the process to exit or services to degrade significantly. CRITICAL logs often trigger automatic page-out workflows.
CRITICAL examples:
- Cannot load configuration or database connection strings
- Required dependency (database, message queue) is unreachable
- Out of memory or system resource exhaustion
- Fatal exception in main loop
- License or security constraint violated
import logging
logger = logging.getLogger(__name__)
def startup():
try:
db = connect_to_database(os.environ['DATABASE_URL'])
logger.info("Database connection established")
except Exception as e:
logger.critical(f"Cannot connect to database: {e}")
raise SystemExit(1)
try:
cache = redis.Redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost'))
logger.info("Redis cache connected")
except Exception as e:
logger.warning(f"Redis unavailable, continuing without cache: {e}")
cache = None
return db, cache
The rule: if the application or service is going to exit or stop serving requests, log CRITICAL and exit. If the application can continue (with degraded function, manual intervention needed later, or a fallback), use ERROR or WARNING.
How Do You Avoid Logging Too Much or Too Little?
Production services typically run at WARNING or INFO level. DEBUG is enabled temporarily during investigation. The balance is: log enough that operators can diagnose issues without re-deploying with more logging, but not so much that the log file grows unbounded or operators are overwhelmed by noise.
For each module, ask: "What events in this module would an operator ask me about in a post-incident conversation?" Log those as INFO. Everything else is DEBUG.
# myapp/payment.py
import logging
logger = logging.getLogger(__name__)
def process_payment(user_id, amount):
logger.debug(f"Process payment called with user_id={user_id}, amount={amount}")
user = get_user(user_id)
if not user:
logger.error(f"User {user_id} not found")
return False
logger.debug(f"User found: {user['email']}")
try:
charge_result = stripe.Charge.create(
amount=int(amount * 100),
currency='usd',
source=user['payment_token']
)
logger.debug(f"Stripe charge created: {charge_result['id']}")
except stripe.CardError as e:
logger.warning(f"Card declined for user {user_id}: {e.user_message}")
return False
except stripe.InvalidRequestError as e:
logger.error(f"Invalid request to Stripe: {e}")
return False
except Exception as e:
logger.critical(f"Unexpected error in payment processing: {e}")
raise
logger.info(f"Payment successful: user {user_id} charged ${amount}")
return True
In this example, DEBUG logs help a developer trace execution, but the operator sees only INFO (payment succeeded), WARNING (card declined), and ERROR/CRITICAL (system failure).
How Do You Filter Logs by Module at Runtime?
You can set different levels for different loggers without restarting the application, allowing selective debugging.
import logging
# Root logger at WARNING
logging.root.setLevel(logging.WARNING)
# But enable DEBUG for specific problematic module
logging.getLogger('myapp.payment').setLevel(logging.DEBUG)
logging.getLogger('myapp.database').setLevel(logging.DEBUG)
# All other modules stay at WARNING
logger = logging.getLogger('myapp.other')
logger.debug("This is not emitted, level is WARNING")
logger2 = logging.getLogger('myapp.payment')
logger2.debug("This IS emitted, level is DEBUG")
Some teams integrate this with HTTP endpoints or environment variables for dynamic reconfiguration:
from flask import Flask, request
app = Flask(__name__)
@app.route('/logging/set-level', methods=['POST'])
def set_log_level():
module = request.json.get('module')
level = request.json.get('level')
logging.getLogger(module).setLevel(getattr(logging, level.upper()))
return {'status': 'ok'}
This is useful for temporary investigation without restarting production services.
Key Takeaways
- DEBUG is for internal diagnostics; INFO is for operational events.
- WARNING means recovery happened; ERROR means the operation failed.
- CRITICAL means the application is unusable and likely to exit.
- Choose levels based on "who needs to know?" not on frequency of events.
- Set module-specific levels at runtime to debug production issues.
Frequently Asked Questions
Should I log every database query?
Log every query as DEBUG (includes the SQL and bind parameters). At production log level (WARNING or INFO), these are filtered out unless a specific module is enabled for debugging.
What level should I use for a user input validation failure?
Validation failure is usually an ERROR: the user's request could not be processed. Log the specific validation error (field name, expected/actual value) as ERROR.
Can I have more than five levels?
Python's logging module defines five standard levels, and you should stick to them for consistency with other libraries and tools. You can add custom levels, but it breaks compatibility.
What is the best way to log user actions (login, purchase, etc.)?
Log these as INFO, as they are operational events that operators and compliance teams care about. Include user identifier and outcome (success/failure), but not sensitive data like passwords.
How do I prevent secret keys from being logged?
Never log sensitive data. Use a filter that redacts passwords, tokens, and API keys before they reach a handler. The logging module supports filters via logger.addFilter().