Right, let’s settle this. You’ve seen basicConfig() everywhere. It’s the logging equivalent of a friendly “Easy” button. And you’ve probably also seen people creating Logger objects, Handler objects, Formatter objects… and thought, “Why would anyone do that the hard way?” I’m here to tell you that basicConfig() is a fantastic one-night stand, but for a serious, long-term relationship with your application’s logs, you need to do things manually. Let’s break down why.

The Siren Song of basicConfig()

logging.basicConfig() is the quickest way to get from zero to having logs show up somewhere. It’s designed for scripts, small tools, and for when you just need to slap some debug prints into a file without a fuss. It does a lot of implicit, “magical” setup for you.

import logging

# The classic. Puts a formatted message to stderr. That's it.
logging.basicConfig(level=logging.INFO)
logging.warning("This will show up.")
logging.info("So will this.")
logging.debug("This, however, is a ghost. You will never see it.")

You can make it slightly more useful:

import logging

# Send it to a file, set a format, and bump the level.
logging.basicConfig(
    filename='app.log',
    filemode='a',  # Default is 'a' (append). Use 'w' to nuke the file each run.
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.DEBUG
)

logging.debug("Hello? Is this thing on?")  # Now this lands in app.log.

Here’s the crucial, often-missed detail: basicConfig() only works once, and only if nothing is configured yet. Call it at the start of your program? Great. Call it twice, or after you’ve already logged something? It silently does nothing. This is the first of many footguns. It’s a one-shot deal for simple setups.

Why You’ll Outgrow It (The Manual Way)

The moment your program evolves past “single file script,” basicConfig() starts to feel like a straitjacket. Real applications have modules, and those modules should have their own named loggers. You want different log levels for different components. You want to send DEBUG to a file but only WARNING to an email. You want to, god forbid, change the configuration at runtime. For this, you need to understand the moving parts: Loggers, Handlers, and Formatters.

import logging

# 1. Create a dedicated logger for your module. __name__ is the convention.
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)  # This logger cares about DEBUG and above.

# 2. Create one or more handlers (where the logs go).
# A FileHandler for detailed traces.
file_handler = logging.FileHandler('detailed.log')
file_handler.setLevel(logging.DEBUG)  # This handler wants DEBUG+

# A StreamHandler for important messages to the console.
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)  # This handler only wants WARNING+

# 3. Create a formatter (what the logs look like).
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 4. Attach the formatter to the handlers.
file_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)

# 5. Attach the handlers to the logger.
logger.addHandler(file_handler)
logger.addHandler(stream_handler)

# Now use it.
logger.debug("This goes only to the file. The console handler ignores it.")
logger.warning("This goes to BOTH the file and the console. Fancy!")

This is more code, yes. But it’s explicit, flexible, and doesn’t rely on any global state magic. You are in complete control.

The Root of All Confusion: The Global Logger

This is the part everyone gets tripped up by. When you do logging.warning("msg"), you’re not using no logger; you’re using the unnamed, pre-configured root logger. basicConfig(), if it works, configures this root logger. This is why its settings feel “global.”

In the manual approach, you avoid the root logger like it’s got the plague. You create your own named loggers (getLogger(__name__)). This creates a hierarchy. A logger named 'project.module' is a child of the logger 'project'. By default, messages propagate up to their parent’s handlers. This is why you often see logs from libraries like urllib3—they’re using their own named loggers, and their messages are propagating up to the root logger, which you might have configured with basicConfig() to output to the console.

The best practice? Never logging.info() in application code. Always get a named logger. This lets you control the verbosity of different parts of your application independently.

The Verdict: When to Use Which

Use logging.basicConfig() when:

  • You’re writing a throwaway script.
  • You’re absolutely, 100% sure your application will never need more than one place to log.
  • You value three lines of code over all future flexibility.

Use manual configuration when:

  • You are writing an actual application (a web app, a GUI, a long-running service).
  • You want different log levels for different components.
  • You need to log to multiple destinations (file, console, syslog, etc.).
  • You value clarity, control, and not wanting to tear your hair out later.

basicConfig() is a tutorial. Manual configuration is the professional-grade tool. Know the difference, and you’ll save yourself a world of logging-induced headaches.