LEARN · DEBUGGING GUIDE

Python logging not showing output: Why your log messages vanish

Your Python logging setup looks correct, but nothing prints. Nine times out of ten, it's the root logger, a handler level filter, or propagation turned off. Here's exactly how to find and fix it.

IntermediatePython8 min read

What this usually means

Python's logging module uses a hierarchy of loggers and handlers. When output is missing, the most common root cause is that the logger you are calling (e.g., logger.info()) has a level that is higher than the message, or the handler(s) attached to it have their own level filter. A subtler issue is propagation: if a child logger has propagation = False and no handlers, the message dies. Another classic: the root logger (logging.getLogger()) starts with level WARNING, so logging.info() on the root logger does nothing. Finally, if you use basicConfig() after any logging call, it has no effect. Misconfigured formatters or missing handler attachment also cause silence.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run python -c "import logging; logging.basicConfig(level=logging.DEBUG); logging.debug('test')" – if nothing prints, Python's logging is broken at the environment level (check stderr redirection).
  • 2Add logging.getLogger().handlers to your code – print them. Empty list means no handler on root logger.
  • 3Check the effective level: logging.getLogger(__name__).getEffectiveLevel(). If > your message level, something upstream is filtering.
  • 4Look for any call to logging.disable(level) – it globally suppresses messages below the given level.
  • 5Search for propagate = False in your code – if a child logger has no handlers and propagation is off, output dies.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe first logging call in your application (often __init__.py or main.py) – basicConfig() must be called before any logging call.
  • searchYour logging configuration file (if using fileConfig or dictConfig) – check handler classes and level settings.
  • searchEnvironment variables like PYTHON_LOG_LEVEL or custom env vars that override logging settings in code.
  • searchThe module __init__.py of your package – sometimes logging gets configured there and overrides later config.
  • searchAny library's own logging setup – e.g., requests, urllib3 – they set their own loggers with levels and propagate settings.
  • searchThe handler output target – file path permissions, syslog daemon status, or stderr redirected in a container.
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningRoot logger level is WARNING by default; info() and debug() calls are ignored.
  • warningbasicConfig() is called after any logging call (including warnings emitted by libraries) – it becomes a no-op.
  • warningCustom logger created with getLogger('x') but no handler added, and propagation = False.
  • warningHandler level is set higher than the logger level – the handler filters messages before they reach the output.
  • warningUsing logging.getLogger().setLevel() on the root logger without a handler – root logger has no handler by default.
  • warningThird-party library sets its own logging configuration that overrides yours (e.g., google.cloud.logging).
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildCall logging.basicConfig(level=logging.DEBUG) at the very start of your application, before any import that might log.
  • buildFor custom loggers, add a handler explicitly: logger.addHandler(logging.StreamHandler()); logger.setLevel(logging.DEBUG).
  • buildSet propagation = True on child loggers that don't have their own handlers, so messages bubble up to the root.
  • buildUse logging.getLogger().handlers to verify handlers; call logging.getLogger().setLevel(logging.NOTSET) to inherit root level.
  • buildIf using dictConfig, ensure the loggers section includes propagate: true and the correct level for each logger.
  • buildCheck for logging.disable() calls and remove them, or set logging.disable(logging.NOTSET) to re-enable.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun your application with PYTHONWARNINGS=always and watch for 'No handlers could be found for logger' warnings.
  • verifiedAdd a temporary logging statement with level CRITICAL – if that prints but INFO doesn't, the level is the issue.
  • verifiedUse logging.getLogger().handlers to list handlers and check their level with handler.level.
  • verifiedSet environment variable PYTHONLOGLEVEL=DEBUG (if your app respects it) and see if output appears.
  • verifiedInsert import logging; logging.basicConfig(level=logging.DEBUG, force=True) at the first line of your entry point to force reconfigure.
  • verifiedCheck the return value of logger.isEnabledFor(level) – if False, something is filtering.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningCalling basicConfig() multiple times – subsequent calls have no effect unless you use force=True (Python 3.8+).
  • warningSetting level on the root logger without adding a handler – messages will be discarded even if level is DEBUG.
  • warningMixing fileConfig and programmatic configuration – one may override the other.
  • warningAssuming logger.info() prints to stdout – it prints to stderr by default. If you redirect stderr, you won't see it.
  • warningForgetting that third-party libraries create their own loggers – you need to configure those specifically.
  • warningUsing logging.getLogger(__name__) inside a module that is imported before logging is configured – the logger gets created with default (WARNING) level.
( 07 )War story

The silent microservice: logging.info() works locally but not in Kubernetes

Backend engineer at a fintech startupPython 3.10, Flask, Kubernetes, ELK stack

Timeline

  1. 09:15Deployed new version of payment-service to staging.
  2. 09:18Alert: payment failures not appearing in Kibana. Users report errors.
  3. 09:22Checked logs via kubectl logs – no output beyond Flask startup messages.
  4. 09:30SSH'd into pod, ran python -c 'import logging; logging.basicConfig(); logging.warning("test")' – output appeared.
  5. 09:35Found in app code: logging.basicConfig(level=logging.INFO) inside an if __name__ == '__main__' block that never executes in Kubernetes (gunicorn runs the app).
  6. 09:40Moved logging config to app factory, added force=True.
  7. 09:45Redeployed, logs appear in Kibana.

Our payment microservice had been running fine for months. A new feature required a logging configuration change, so I added logging.basicConfig(level=logging.INFO) in the main module. I tested locally with python app.py and saw logs. But after deploying to staging, nothing showed up in Kibana. Users reported payment failures, but we had no trace of them.

I started by checking if the service was even running – it was. kubectl logs showed only the Flask startup messages, not our application logs. I suspected the logging config wasn't taking effect. I SSH'd into a pod and ran a quick Python test – that worked. So the issue was specific to the application's logging setup.

Then I remembered: in Kubernetes, we run the app with gunicorn, not python app.py. The if __name__ == '__main__' block never runs. The logging.basicConfig() call was never executed. I moved the configuration to an app factory function that runs on every worker start, and added force=True to override any prior config (in case Flask itself had set up logging). Redeployed, and logs appeared immediately.

Root cause

Logging.basicConfig() placed inside if __name__ == '__main__' block, which is not executed when the module is imported by gunicorn.

The fix

Moved logging configuration to the app factory function, called before the Flask app is created. Used logging.basicConfig(level=logging.INFO, force=True) to ensure it overrides any earlier configuration.

The lesson

Never rely on __main__ blocks for production configuration. Always initialize logging in a module that runs on import, and use force=True to guard against competing configurations.

( 08 )How Python's logging hierarchy silences your messages

Python's logging module has a tree structure: the root logger is the top, and all other loggers are children. When you call logger.info('msg'), the logger checks its own level (set via setLevel()). If the message level is below the logger's threshold, it's discarded immediately. If it passes, the logger creates a LogRecord and passes it to its handlers. Each handler also has a level filter; if the record level is below the handler's level, the handler ignores it. Finally, if the logger's propagate flag is True (default), the record is also passed to the parent logger's handlers. If propagate is False and the logger has no handlers, the record is discarded.

The root logger starts with level WARNING and no handlers. So logging.info() on the root logger does nothing because INFO < WARNING. Adding a handler to the root logger without lowering its level still won't show INFO messages. The fix: set the root logger's level to DEBUG (or INFO) and add a handler. Alternatively, create a child logger with its own level and handler.

( 09 )basicConfig() quirks you must know

logging.basicConfig() is a convenience function that configures the root logger with a StreamHandler and a formatter. But it only works if the root logger has no handlers already. That means any prior call to logging.getLogger().addHandler() or any library that sets up logging (like Flask or Django) will make basicConfig() a no-op. In Python 3.8+, you can force it with basicConfig(force=True), which removes all existing handlers first.

Another trap: basicConfig() must be called before any logging call. The first time a logger is used (e.g., via logging.info()), the logging module does a one-time configuration that locks in the root logger's state. After that, basicConfig() is ignored. This is why placing basicConfig() after imports that log (like third-party libraries) often fails.

( 10 )Diagnosing propagation issues with custom loggers

If you use logging.getLogger('myapp') and set its level to DEBUG but see no output, check propagation. If myapp has no handlers and propagation is True, the record goes to the root logger. If the root logger has no handlers or a high level, still nothing. If propagation is False, the record is dropped entirely. The solution: either add a handler to myapp, set propagation True and ensure the root logger is configured, or both.

To inspect propagation: print(logger.propagate). Use logger.handlers to see if any handlers exist. The effective level can be found with logger.getEffectiveLevel(), which traverses the hierarchy up to the root if the logger's own level is NOTSET. If effective level is 30 (WARNING) but you set the logger to DEBUG, it means a parent logger has a higher level.

( 11 )Third-party library loggers: why they stay silent

Many popular libraries create their own loggers with names like 'urllib3', 'requests', 'boto3', etc. These loggers often start with level WARNING and no handlers. If you want to see their debug output, you need to configure them explicitly. For example: logging.getLogger('urllib3').setLevel(logging.DEBUG) and ensure there's a handler attached (either to that logger or to the root with propagation).

A common mistake is to set the root logger to DEBUG and assume all library logs will appear. But if a library logger has its own handlers (like some do), propagation doesn't apply. Check the library's documentation. Also, some libraries use their own logging configuration that may override your settings – for example, google.cloud.logging sets up its own handler that captures all logs.

( 12 )Container and cloud environment gotchas

In Docker or Kubernetes, stdout and stderr are captured by the container runtime. Python's StreamHandler writes to stderr by default. If your container's logging driver only captures stdout, you'll miss logs. Use StreamHandler(sys.stdout) explicitly or configure your container to capture stderr.

Some cloud logging agents (like Fluentd or the AWS CloudWatch agent) parse log files. If you write to a file, ensure the file path is within the agent's monitored directory and that the file is rotated properly. Permission issues are common: the application runs as a non-root user and can't write to /var/log. Use a writable directory like /tmp or mount a volume.

Frequently asked questions

Why does logging.info() print nothing even after I called basicConfig()?

Most likely, basicConfig() was called after a logging call (e.g., from an imported module). The first logging call triggers a one-time configuration that locks the root logger. Subsequent basicConfig() calls are ignored. Use basicConfig(force=True) in Python 3.8+ to force reconfiguration, or move the call to the very beginning of your script.

I set logger.setLevel(logging.DEBUG) but still no output. What's wrong?

Check the handler's level. Each handler also has its own level. If the handler's level is WARNING and the logger's level is DEBUG, debug messages will pass the logger but be filtered out by the handler. Use handler.setLevel(logging.DEBUG) or remove the handler and add a new one. Also verify that the logger has any handler attached.

How do I see logs from third-party libraries like requests or urllib3?

You need to configure their loggers explicitly. For example: logging.getLogger('urllib3').setLevel(logging.DEBUG). Ensure there is a handler attached either to that logger or to its ancestors (root) with propagation enabled. Also check if the library itself adds handlers – some do, which can interfere with your configuration.

What does 'No handlers could be found for logger' mean?

It means a logger (usually a child logger) has no handlers and propagation is False, causing the log record to be discarded. The warning appears only once per logger. To fix, either add a handler to that logger, set propagate=True (default), or configure the root logger with a handler so that the record bubbles up.

Why do my logs work in local development but not in production (Docker/K8s)?

Common causes: (1) The entry point differs (e.g., gunicorn vs python app.py) and your logging config is in an if __name__ == '__main__' block. (2) Stderr is not captured in your container logging driver. (3) Environment variables override your logging level. (4) File permissions prevent writing to a log file. Always test with the exact production entry point.