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,…
- CoursePython standard library deep dive
- Lesson13 of 24
- Video15 min
- FormatJupyter notebook · 10 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbPython 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')
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)
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')
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')
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')
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())
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()
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')
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')
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)
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.



