Fix: Python logging Not Showing Output
Quick Answer
How to fix Python logging not displaying messages — log level misconfiguration, missing handlers, root logger vs named loggers, basicConfig not working, and logging in libraries vs applications.
The Error
You call logging.info() or logger.debug() but nothing appears in the output:
import logging
logging.info("Starting application") # Nothing printed
logging.debug("Debug message") # Nothing printedOr a named logger produces no output:
import logging
logger = logging.getLogger(__name__)
logger.info("This should appear") # SilenceOr only some log levels appear:
logging.basicConfig()
logging.debug("debug") # Nothing
logging.info("info") # Nothing
logging.warning("warn") # WARNING:root:warn ← Only warnings and aboveOr the configuration you set is being ignored:
logging.basicConfig(level=logging.DEBUG) # Configured DEBUG level
logging.debug("debug") # Still nothing — basicConfig ignored?Why This Happens
Python’s logging system has multiple layers — loggers, handlers, and formatters — each with their own level settings. Misconfiguration at any layer causes messages to disappear:
- Default level is WARNING — the root logger’s default level is
WARNING.DEBUGandINFOmessages are filtered out unless you explicitly lower the level. basicConfig()called after handlers are attached —basicConfig()is a no-op if the root logger already has handlers. Calling it after any logging output (even from a library) means your configuration is silently ignored.- No handler attached — a logger with no handler (and no propagation to a logger with a handler) drops all messages silently.
- Handler level vs logger level — both the logger AND the handler have levels. A message must pass both filters. Setting
logger.setLevel(DEBUG)but leaving the handler atWARNINGstill suppresses debug messages. propagate=Falseon a named logger without its own handler — if you setpropagate=Falseon a logger but don’t attach a handler, the logger has nowhere to send messages.- Library calling
logging.basicConfig()— some libraries callbasicConfig()themselves, consuming the root logger’s handler slot before your code runs.
Fix 1: Set the Log Level Explicitly
The most common fix — set the log level on both the root logger and the handler:
import logging
# Configure root logger
logging.basicConfig(
level=logging.DEBUG, # Show DEBUG and above
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logging.debug("debug message") # DEBUG:root:debug message
logging.info("info message") # INFO:root:info message
logging.warning("warning") # WARNING:root:warningFor named loggers:
import logging
# Configure root logger first
logging.basicConfig(level=logging.DEBUG)
# Get a named logger — inherits root's handler and level
logger = logging.getLogger(__name__)
logger.debug("this now works") # Shows if basicConfig was called firstThe level hierarchy: A message must pass the logger’s level check AND the handler’s level check to appear in output. If either is set too high, the message is silently dropped.
Fix 2: Call basicConfig Before Any Logging Occurs
basicConfig() is a no-op if the root logger already has handlers. Import order matters:
import logging
# WRONG — some libraries log during import, adding handlers before basicConfig
import requests # requests may trigger logging setup
import boto3 # boto3 adds its own handlers
logging.basicConfig(level=logging.DEBUG) # Ignored — handlers already exist!
# CORRECT — configure logging FIRST, before importing third-party libraries
import logging
logging.basicConfig(level=logging.DEBUG) # Runs before any handlers are added
import requests
import boto3Or force reconfiguration with force=True (Python 3.8+):
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(levelname)s:%(name)s:%(message)s',
force=True, # Removes existing handlers and reconfigures
)force=True is the nuclear option — it clears all existing handlers and reapplies the configuration. Use it when you can’t control import order.
Fix 3: Add a Handler Manually
When basicConfig() isn’t appropriate (library code, complex apps), configure handlers explicitly:
import logging
import sys
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG) # Logger level
# Create a console handler
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG) # Handler level — must also be DEBUG
# Create a formatter
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
handler.setFormatter(formatter)
# Attach handler to logger
logger.addHandler(handler)
# Now it works
logger.debug("debug")
logger.info("info")
logger.error("error")Add a file handler:
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.WARNING) # Only warnings and above to file
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Sends to stdout AND file (with different levels)
logger.warning("This goes to both stdout and app.log")
logger.debug("This only goes to stdout")Fix 4: Fix the Logger Hierarchy and Propagation
Named loggers form a hierarchy based on their names. logging.getLogger('myapp.db') is a child of logging.getLogger('myapp'), which is a child of the root logger:
import logging
# Root logger — parent of all loggers
root = logging.getLogger()
root.setLevel(logging.WARNING) # Root filters to WARNING and above
# Named logger
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG) # Logger allows DEBUG
# Handler on root — this is the only handler
logging.basicConfig(level=logging.WARNING)
logger.debug("debug") # Passes logger level (DEBUG) but FAILS root handler level (WARNING)
logger.warning("warn") # Passes both → appearsFix: set the handler level to match what you want to see:
import logging
logging.basicConfig(level=logging.DEBUG) # Handler level = DEBUG
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG) # Logger level = DEBUG
logger.debug("now visible") # Passes both levels ✓Or add a handler directly to the named logger:
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.propagate = False # Don't also send to root logger (prevents duplicate output)
logger.debug("works")Fix 5: Fix Duplicate Log Messages
If messages appear twice in the output, a handler is attached to both a logger and its parent:
import logging
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)
# Handler added to 'myapp' logger
handler = logging.StreamHandler()
logger.addHandler(handler)
# basicConfig also adds a handler to the root logger
logging.basicConfig()
logger.info("test")
# Output:
# test ← from 'myapp' handler
# INFO:myapp:test ← propagated to root handler (with default format)Fix — disable propagation to prevent double logging:
logger = logging.getLogger('myapp')
logger.addHandler(handler)
logger.propagate = False # Don't send to root loggerFix 6: Configure Logging with dictConfig
For production applications, use dictConfig for structured, easy-to-read configuration:
import logging
import logging.config
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False, # Don't silence library loggers
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
},
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter', # pip install python-json-logger
'format': '%(asctime)s %(levelname)s %(name)s %(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'standard',
'stream': 'ext://sys.stdout',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'WARNING',
'formatter': 'standard',
'filename': 'app.log',
'maxBytes': 10_485_760, # 10 MB
'backupCount': 5,
},
},
'loggers': {
'': { # Root logger
'handlers': ['console'],
'level': 'WARNING',
},
'myapp': { # Application logger
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False, # Don't also log to root
},
'myapp.db': { # Database logger — less verbose
'level': 'INFO',
'propagate': True, # Uses myapp's handlers
},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger('myapp')
logger.debug("Application starting") # ✓ visibleFix 7: Silence Noisy Library Loggers
Third-party libraries (requests, boto3, urllib3, sqlalchemy) often log at DEBUG level, flooding your output when you enable DEBUG for your app:
import logging
# Enable DEBUG for your code only
logging.basicConfig(level=logging.WARNING) # Root stays at WARNING
# Enable DEBUG for your specific module
logging.getLogger('myapp').setLevel(logging.DEBUG)
# Silence specific noisy libraries
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('boto3').setLevel(logging.WARNING)
logging.getLogger('botocore').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
logging.getLogger('requests').setLevel(logging.WARNING)In dictConfig, control third-party loggers:
'loggers': {
'urllib3': {'level': 'WARNING'},
'boto3': {'level': 'WARNING'},
'sqlalchemy.engine': {'level': 'INFO'}, # Show SQL queries but not debug
}Still Not Working?
Inspect what handlers are attached:
import logging
# Check root logger
root = logging.getLogger()
print("Root level:", root.level, logging.getLevelName(root.level))
print("Root handlers:", root.handlers)
# Check named logger
logger = logging.getLogger('myapp')
print("Logger level:", logger.level, logging.getLevelName(logger.level))
print("Logger handlers:", logger.handlers)
print("Propagate:", logger.propagate)
# Walk the effective hierarchy
log = logger
while log:
print(f"Logger '{log.name}': level={logging.getLevelName(log.level)}, handlers={log.handlers}")
if not log.parent:
break
log = log.parentUse logging.getLogger().manager.loggerDict to see all loggers that have been created:
print(logging.getLogger().manager.loggerDict.keys())Check if a library is using NullHandler — libraries should call logging.getLogger(__name__).addHandler(logging.NullHandler()) and let the application configure logging. If a library adds a real handler, it may conflict with your setup.
For related Python issues, see Fix: Python asyncio Runtime Error and Fix: Python ImportError Circular Import.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors
How to fix Python packaging issues — pyproject.toml setup, build backends (setuptools/hatchling/flit), wheel vs sdist, editable installs, package discovery, and twine upload to PyPI.
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.