Mathew K Analytics

Lesson 13 · Python standard library deep dive

Python logging Explained: Loggers, Handlers & Formatters | Standard Library #13

Video thirteen of the twenty-five-part series: logging, Python's built-in, production-grade diagnostics system. Levels, loggers, formatters, handlers,…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Python Standard Library Deep-Dive, Video 13: logging#

  • Video thirteen of the twenty-five-part series: logging, Python's built-in, production-grade diagnostics system.
  • Levels, loggers, formatters, handlers, exceptions, and the logger hierarchy.
  • Let's get into it.

Part 1: What logging Offers, vs print()#

import logging
logging.warning('This is a warning, using the root logger directly')
logging.info('This info message genuinely will not show by default')
WARNING:root:This is a warning, using the root logger directly

Part 2: Log Levels#

print(logging.DEBUG)
print(logging.INFO)
print(logging.WARNING)
print(logging.ERROR)
print(logging.CRITICAL)
print(logging.DEBUG < logging.INFO < logging.WARNING < logging.ERROR < logging.CRITICAL)
10
20
30
40
50
True

Part 3: basicConfig()#

import importlib
importlib.reload(logging)
logging.basicConfig(
    level=logging.DEBUG,
    format='%(levelname)s: %(message)s'
)
logging.debug('Now debug messages genuinely show up')
logging.info('So do info messages')
logging.warning('And warnings still show, formatted the same way')
DEBUG: Now debug messages genuinely show up
INFO: So do info messages
WARNING: And warnings still show, formatted the same way

Part 4: getLogger() and name#

logger = logging.getLogger('my_app.database')
print(logger.name)
same_logger = logging.getLogger('my_app.database')
print(logger is same_logger)
logger.warning('A warning from the database module specifically')
WARNING: A warning from the database module specifically
my_app.database
True

Part 5: Formatters#

formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
custom_logger = logging.getLogger('demo.formatter')
custom_logger.addHandler(handler)
custom_logger.setLevel(logging.INFO)
custom_logger.propagate = False
custom_logger.info('A fully custom-formatted message')
2026-08-23 18:07:23,802 | demo.formatter | INFO | A fully custom-formatted message

Part 6: Handlers: StreamHandler and FileHandler#

file_logger = logging.getLogger('demo.file')
file_logger.propagate = False
file_handler = logging.FileHandler('demo_app.log')
file_handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
file_logger.addHandler(file_handler)
file_logger.setLevel(logging.INFO)
file_logger.info('This genuinely goes to a real file, not the console')
file_handler.close()
with open('demo_app.log') as f:
    print(f.read())
INFO - This genuinely goes to a real file, not the console

Part 7: Multiple Handlers on One Logger#

multi_logger = logging.getLogger('demo.multi')
multi_logger.propagate = False
multi_logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
file_handler2 = logging.FileHandler('demo_detailed.log')
file_handler2.setLevel(logging.DEBUG)
file_handler2.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
multi_logger.addHandler(console_handler)
multi_logger.addHandler(file_handler2)
multi_logger.debug('Only genuinely goes to the file')
multi_logger.error('Goes to both the console and the file')
file_handler2.close()
ERROR: Goes to both the console and the file

Part 8: Logging Exceptions#

error_logger = logging.getLogger('demo.errors')
error_logger.propagate = False
error_logger.addHandler(logging.StreamHandler())
error_logger.setLevel(logging.ERROR)
try:
    result = 10 / 0
except ZeroDivisionError:
    error_logger.exception('A calculation genuinely failed')
A calculation genuinely failed
Traceback (most recent call last):
  File "C:\Users\makmw\AppData\Local\Temp\ipykernel_25540\346908998.py", line 6, in <module>
    result = 10 / 0
             ~~~^~~
ZeroDivisionError: division by zero

Part 9: Logger Hierarchy and Propagation#

parent = logging.getLogger('myapp')
parent.propagate = False
parent.addHandler(logging.StreamHandler())
parent.setLevel(logging.INFO)
child = logging.getLogger('myapp.submodule')
print(child.parent is parent)
child.info('This message genuinely propagates up to the parent handler')
This message genuinely propagates up to the parent handler
True

Part 10: Common Patterns#

def process_order(order_id, amount):
    module_logger = logging.getLogger('orders')
    module_logger.propagate = False
    if not module_logger.handlers:
        module_logger.addHandler(logging.StreamHandler())
        module_logger.setLevel(logging.INFO)
    module_logger.info(f'Processing order {order_id} for ${amount}')
    if amount <= 0:
        module_logger.warning(f'Order {order_id} has a genuinely suspicious amount')
        return False
    module_logger.info(f'Order {order_id} processed successfully')
    return True
process_order(1001, 49.99)
process_order(1002, -5.00)
Processing order 1001 for $49.99
Order 1001 processed successfully
Processing order 1002 for $-5.0
Order 1002 has a genuinely suspicious amount
False

Wrap-Up: What You Learned#

  • logging adds severity, timestamps, and structure that print() simply can't offer.
  • Five levels: DEBUG, INFO, WARNING, ERROR, CRITICAL, in increasing severity.
  • basicConfig for one-time root logger setup; getLogger(name) for real, per-module loggers.
  • Formatters control exact message layout; handlers control where messages actually go.
  • StreamHandler for console output, FileHandler for real files, and multiple handlers per logger.
  • logger.exception automatically attaches the full traceback inside an except block.
  • The dotted logger hierarchy and propagate, letting one parent's handlers cover every submodule.
  • The real production pattern: configure once at the entry point, getLogger(name) everywhere else.
  • That wraps up logging. Next up: argparse, for building real command-line interfaces.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.