{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "title",
   "metadata": {},
   "source": [
    "# Your first log message\n",
    "\n",
    "In this tutorial, you will learn to use the `logging` module in Python to write log messages.\n",
    "By the end, you will understand why logging is more useful than `print()` for diagnostic output,\n",
    "and you will be able to configure basic logging and create named loggers.\n",
    "\n",
    "**Time commitment:** 15--20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- Python 3.12 or later installed\n",
    "- Basic Python knowledge (variables, functions, imports)\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Explain why the `logging` module is more useful than `print()` for diagnostic output\n",
    "- Use `logging.basicConfig()` to set up quick logging configuration\n",
    "- Write log messages at different severity levels\n",
    "- Create a named logger with `logging.getLogger()`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "why-logging",
   "metadata": {},
   "source": [
    "## Why use logging?\n",
    "\n",
    "When something goes wrong in a program, how do you find out what happened? Many developers\n",
    "reach for `print()` first. It is quick, familiar, and gets the job done for small scripts.\n",
    "\n",
    "However, `print()` has limitations:\n",
    "\n",
    "- There is no way to filter messages by severity\n",
    "- Output always goes to the same place (standard output)\n",
    "- There is no easy way to disable diagnostic output in production\n",
    "- Messages lack context such as timestamps, source locations, and severity\n",
    "\n",
    "The `logging` module in Python solves all of these problems. It is part of the standard library,\n",
    "so there is nothing extra to install."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "print-vs-logging-intro",
   "metadata": {},
   "source": [
    "### A quick comparison\n",
    "\n",
    "Let us compare `print()` and `logging` side by side."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "print-example",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Using print() for diagnostic output\n",
    "print(\"Starting data processing\")\n",
    "print(\"Processed 10 items\")\n",
    "print(\"WARNING: 2 items were skipped\")\n",
    "print(\"ERROR: Could not save results\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "logging-example",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "\n",
    "# Using logging for diagnostic output\n",
    "logging.info(\"Starting data processing\")\n",
    "logging.info(\"Processed 10 items\")\n",
    "logging.warning(\"2 items were skipped\")\n",
    "logging.error(\"Could not save results\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "comparison-explanation",
   "metadata": {},
   "source": [
    "Notice that only `WARNING` and `ERROR` appeared in the logging output. By default, the\n",
    "`logging` module only shows messages at `WARNING` level or above. The `info()` messages\n",
    "were silently filtered out.\n",
    "\n",
    "This is one of the key advantages of logging: you can control which messages appear\n",
    "without changing your code."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "import-logging",
   "metadata": {},
   "source": [
    "## The `logging` module\n",
    "\n",
    "The `logging` module is built into Python. You do not need to install anything &ndash; just\n",
    "import it and start using it.\n",
    "\n",
    "The simplest way to use logging is through the module-level convenience functions:\n",
    "\n",
    "- `logging.debug()` -- Detailed diagnostic information\n",
    "- `logging.info()` -- General information about program operation\n",
    "- `logging.warning()` -- Something unexpected happened (this is the default level)\n",
    "- `logging.error()` -- An error occurred but the program can continue\n",
    "- `logging.critical()` -- A serious error; the program may not be able to continue"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "warning-default",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "\n",
    "# By default, only WARNING and above are shown\n",
    "logging.debug(\"This will NOT appear\")\n",
    "logging.info(\"This will NOT appear either\")\n",
    "logging.warning(\"This WILL appear\")\n",
    "logging.error(\"This WILL appear too\")\n",
    "logging.critical(\"This WILL also appear\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "basicconfig-section",
   "metadata": {},
   "source": [
    "## Configuring logging with `basicConfig()`\n",
    "\n",
    "To see messages at all levels, including `DEBUG` and `INFO`, you need to configure the\n",
    "logging system. The quickest way is with `logging.basicConfig()`.\n",
    "\n",
    "The `level` parameter controls the minimum severity of messages that will be shown."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "basicconfig-debug",
   "metadata": {},
   "outputs": [],
   "source": [
    "import importlib\n",
    "import logging\n",
    "\n",
    "# Reset the logging module so basicConfig() takes effect\n",
    "importlib.reload(logging)\n",
    "\n",
    "# Configure logging to show all messages from DEBUG level upwards\n",
    "logging.basicConfig(level=logging.DEBUG)\n",
    "\n",
    "logging.debug(\"This is a DEBUG message\")\n",
    "logging.info(\"This is an INFO message\")\n",
    "logging.warning(\"This is a WARNING message\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "basicconfig-note",
   "metadata": {},
   "source": [
    "Now all three messages appear, because we set the level to `logging.DEBUG`.\n",
    "\n",
    "**Important:** `logging.basicConfig()` only takes effect the first time it is called.\n",
    "If the logging system has already been configured (for example, by a previous call), subsequent\n",
    "calls to `basicConfig()` have no effect. In these tutorial cells, we use `importlib.reload(logging)`\n",
    "to reset the module so we can demonstrate different configurations."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "format-section",
   "metadata": {},
   "source": [
    "### Adding a format\n",
    "\n",
    "You can also customise the format of log messages using the `format` parameter.\n",
    "Format strings use special placeholders such as `%(asctime)s` for the timestamp,\n",
    "`%(levelname)s` for the severity level, and `%(message)s` for the actual message."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "basicconfig-format",
   "metadata": {},
   "outputs": [],
   "source": [
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=logging.DEBUG,\n",
    "    format=\"%(asctime)s - %(levelname)s - %(message)s\"\n",
    ")\n",
    "\n",
    "logging.info(\"Application started\")\n",
    "logging.warning(\"Disk space is running low\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "named-loggers-section",
   "metadata": {},
   "source": [
    "## Creating a named logger\n",
    "\n",
    "The module-level functions (`logging.info()`, `logging.warning()`, and so on) all use\n",
    "the **root logger**. For anything beyond a simple script, it is better to create a\n",
    "**named logger** using `logging.getLogger()`.\n",
    "\n",
    "Named loggers have several advantages:\n",
    "\n",
    "- You can identify which part of your application produced each message\n",
    "- You can configure different loggers independently\n",
    "- Loggers form a hierarchy based on their names, using dots as separators\n",
    "\n",
    "The standard convention is to use `__name__` as the logger name, which automatically\n",
    "gives the logger the name of the current module."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "named-logger",
   "metadata": {},
   "outputs": [],
   "source": [
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=logging.DEBUG,\n",
    "    format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n",
    ")\n",
    "\n",
    "# Create a named logger\n",
    "logger = logging.getLogger(\"my_application\")\n",
    "\n",
    "logger.info(\"Application started\")\n",
    "logger.warning(\"Something needs attention\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "named-logger-explanation",
   "metadata": {},
   "source": [
    "Notice the `%(name)s` field now shows `my_application` in the output. This makes it\n",
    "easy to trace which component produced each log message, especially in larger applications\n",
    "with many modules."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "practical-example-section",
   "metadata": {},
   "source": [
    "## Putting it all together\n",
    "\n",
    "Let us write a small function that processes a list of numbers and uses logging\n",
    "to report what it is doing. This is a more realistic example of how logging works\n",
    "in practice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "practical-example",
   "metadata": {},
   "outputs": [],
   "source": [
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=logging.DEBUG,\n",
    "    format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n",
    ")\n",
    "\n",
    "logger = logging.getLogger(\"data_processor\")\n",
    "\n",
    "\n",
    "def process_numbers(numbers: list[int]) -> list[int]:\n",
    "    \"\"\"Double each number in the list, skipping negative values.\n",
    "\n",
    "    Args:\n",
    "        numbers: A list of integers to process.\n",
    "\n",
    "    Returns:\n",
    "        A list of doubled positive integers.\n",
    "    \"\"\"\n",
    "    logger.info(\"Starting processing of %s numbers\", len(numbers))\n",
    "    results = []\n",
    "\n",
    "    for number in numbers:\n",
    "        if number < 0:\n",
    "            logger.warning(\"Skipping negative number: %s\", number)\n",
    "            continue\n",
    "        doubled = number * 2\n",
    "        logger.debug(\"Doubled %s to %s\", number, doubled)\n",
    "        results.append(doubled)\n",
    "\n",
    "    logger.info(\"Processing complete: %s results produced\", len(results))\n",
    "    return results\n",
    "\n",
    "\n",
    "output = process_numbers([1, 5, -3, 10, -7, 4])\n",
    "print(\"Results:\", output)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "practical-explanation",
   "metadata": {},
   "source": [
    "Notice several things about this example:\n",
    "\n",
    "- We use `%s` placeholders instead of f-strings in log calls. This is important because\n",
    "  `%s` formatting is **lazy** -- the string is only formatted if the message will actually\n",
    "  be emitted. With f-strings, the formatting always happens, even if the log level means\n",
    "  the message will be discarded.\n",
    "- Different log levels convey different meanings: `INFO` for normal progress, `WARNING`\n",
    "  for unexpected situations, and `DEBUG` for detailed diagnostic information.\n",
    "- The `print()` call at the end is for the actual program output, which is separate from\n",
    "  the diagnostic logging."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercises-section",
   "metadata": {},
   "source": [
    "## Exercises\n",
    "\n",
    "Try these exercises to reinforce what you have learned."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-1",
   "metadata": {},
   "source": [
    "### Exercise 1: Configure and log\n",
    "\n",
    "Configure `basicConfig()` to show messages at `INFO` level and above, with a format\n",
    "that includes the level name and message. Then log one message at each of the five levels.\n",
    "Which messages appear?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "exercise-1-solution",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 1: Solution\n",
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(level=logging.INFO, format=\"%(levelname)s - %(message)s\")\n",
    "\n",
    "logging.debug(\"Debug message\")       # Will NOT appear (below INFO)\n",
    "logging.info(\"Info message\")          # Will appear\n",
    "logging.warning(\"Warning message\")    # Will appear\n",
    "logging.error(\"Error message\")        # Will appear\n",
    "logging.critical(\"Critical message\")  # Will appear"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-2",
   "metadata": {},
   "source": [
    "### Exercise 2: Named logger\n",
    "\n",
    "Create a named logger called `\"weather_app\"`. Configure logging to include the logger\n",
    "name in the output. Use the logger to log an `INFO` message and a `WARNING` message."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "exercise-2-solution",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 2: Solution\n",
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=logging.DEBUG,\n",
    "    format=\"%(name)s - %(levelname)s - %(message)s\"\n",
    ")\n",
    "\n",
    "logger = logging.getLogger(\"weather_app\")\n",
    "\n",
    "logger.info(\"Fetching weather data for London\")\n",
    "logger.warning(\"API rate limit is approaching: %s of %s requests used\", 95, 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exercise-3",
   "metadata": {},
   "source": [
    "### Exercise 3: Add logging to a function\n",
    "\n",
    "Write a function called `divide(a, b)` that returns the result of dividing `a` by `b`.\n",
    "Use logging to:\n",
    "\n",
    "- Log an `INFO` message when the function is called, showing the arguments\n",
    "- Log an `ERROR` message if `b` is zero\n",
    "- Log a `DEBUG` message showing the result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "exercise-3-solution",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 3: Solution\n",
    "import importlib\n",
    "import logging\n",
    "\n",
    "importlib.reload(logging)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=logging.DEBUG,\n",
    "    format=\"%(levelname)s - %(message)s\"\n",
    ")\n",
    "\n",
    "logger = logging.getLogger(\"calculator\")\n",
    "\n",
    "\n",
    "def divide(a: float, b: float) -> float | None:\n",
    "    \"\"\"Divide a by b, returning None if b is zero.\n",
    "\n",
    "    Args:\n",
    "        a: The numerator.\n",
    "        b: The denominator.\n",
    "\n",
    "    Returns:\n",
    "        The result of a / b, or None if b is zero.\n",
    "    \"\"\"\n",
    "    logger.info(\"Dividing %s by %s\", a, b)\n",
    "\n",
    "    if b == 0:\n",
    "        logger.error(\"Cannot divide by zero\")\n",
    "        return None\n",
    "\n",
    "    result = a / b\n",
    "    logger.debug(\"Result: %s\", result)\n",
    "    return result\n",
    "\n",
    "\n",
    "divide(10, 3)\n",
    "divide(5, 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "summary",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this tutorial, you learned the following:\n",
    "\n",
    "- The `logging` module provides structured diagnostic output with severity levels,\n",
    "  timestamps, and configurable destinations\n",
    "- `logging.basicConfig()` is the quickest way to configure logging, with parameters\n",
    "  for `level` and `format`\n",
    "- The five log levels, from least to most severe, are `DEBUG`, `INFO`, `WARNING`,\n",
    "  `ERROR`, and `CRITICAL`\n",
    "- By default, only `WARNING` and above are shown\n",
    "- Named loggers (created with `logging.getLogger()`) help identify which part of\n",
    "  your application produced each message\n",
    "- Use `%s` formatting in log calls, not f-strings, for lazy evaluation\n",
    "\n",
    "**Next tutorial:** Continue to [Log levels and formatting](https://agilearn.co.uk/guides/logging-and-debugging/learn/02-log-levels-and-formatting)\n",
    "to explore log levels in depth and learn how to customise the format of your log messages."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}