Skip to content

Fix: Python logging Not Showing Output

FixDevs ·

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 printed

Or a named logger produces no output:

import logging

logger = logging.getLogger(__name__)
logger.info("This should appear")  # Silence

Or only some log levels appear:

logging.basicConfig()
logging.debug("debug")    # Nothing
logging.info("info")      # Nothing
logging.warning("warn")   # WARNING:root:warn ← Only warnings and above

Or 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. DEBUG and INFO messages are filtered out unless you explicitly lower the level.
  • basicConfig() called after handlers are attachedbasicConfig() 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 at WARNING still suppresses debug messages.
  • propagate=False on a named logger without its own handler — if you set propagate=False on a logger but don’t attach a handler, the logger has nowhere to send messages.
  • Library calling logging.basicConfig() — some libraries call basicConfig() 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:warning

For 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 first

The 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 boto3

Or 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 → appears

Fix: 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 logger

Fix 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")   # ✓ visible

Fix 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.parent

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

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles