EASYTUTORGUIDE

Practical tutorials, tools, courses, digital skills, and business promotion.

Free Learning

JavaScript – Chapter 2: Development Environment

Learn what JavaScript is, where it runs, why it is important, and how beginners can start using it with simple examples and clear output.

Beginner Friendly JavaScript Basics Web Development Code Examples
JavaScript Lesson 2 Chapter 2 Topics Day / Night Mode

Main reading content

python-course-chapter-39

Chapter 39: Logging and Application Configuration

Learn how to record application events, manage settings, use environment variables, protect secrets, and create reliable configuration systems.

Goal: Build Python applications that produce useful log messages and safely load different settings for development and production environments.

Chapter 39 Topics

39.1 Why Logging Matters

Logging means recording important events that happen while a program is running. Log messages can show when an application starts, which actions were completed, what values were processed, and why an error occurred. Logging is more useful than adding many temporary print statements because logs can include dates, severity levels, module names, and permanent records.

Example: Record important program events

# Import Python's built-in logging module.
import logging

# Configure basic logging.
logging.basicConfig(level=logging.INFO)

# Record that the application has started.
logging.info("Application started.")

# Store a sample order number.
order_number = 105

# Record that an order is being processed.
logging.info("Processing order %s.", order_number)

# Record that the operation completed.
logging.info("Order processed successfully.")
Output:
INFO:root:Application started.
INFO:root:Processing order 105.
INFO:root:Order processed successfully.

Explanation: Each line contains the log level, the logger name, and the message. These messages provide a simple history of the actions completed by the program.

39.2 The logging Module

Python includes a built-in module named logging. It provides functions and classes for recording application messages. Beginners can start with functions such as logging.info(), logging.warning(), and logging.error(). Larger applications can create named loggers, handlers, formatters, filters, and configuration files for more control.

Example: Use the basic logging functions

# Import the logging module.
import logging

# Show messages at DEBUG level and above.
logging.basicConfig(level=logging.DEBUG)

# Record detailed development information.
logging.debug("The user list is being loaded.")

# Record normal application information.
logging.info("The user list loaded successfully.")

# Record a possible problem.
logging.warning("One user profile is incomplete.")

# Record an operation failure.
logging.error("A profile image could not be opened.")

# Record a very serious failure.
logging.critical("The main database is unavailable.")
Output:
DEBUG:root:The user list is being loaded.
INFO:root:The user list loaded successfully.
WARNING:root:One user profile is incomplete.
ERROR:root:A profile image could not be opened.
CRITICAL:root:The main database is unavailable.

Explanation: The logging module provides different methods for different levels of importance. Because the configured level is DEBUG, every message in this example is displayed.

39.3 Log Levels

Log levels describe how important or serious a message is. The standard levels are DEBUG, INFO, WARNING, ERROR, and CRITICAL. Setting a logging level filters messages. For example, an INFO setting displays information, warnings, errors, and critical messages, but normally hides debug details.

Example: Filter messages using the INFO level

# Import the logging module.
import logging

# Display INFO messages and anything more serious.
logging.basicConfig(level=logging.INFO)

# This message is hidden because DEBUG is below INFO.
logging.debug("Detailed variable information.")

# This message is displayed.
logging.info("The report was created.")

# This message is displayed.
logging.warning("The report contains missing values.")

# This message is displayed.
logging.error("The report could not be emailed.")
Output:
INFO:root:The report was created.
WARNING:root:The report contains missing values.
ERROR:root:The report could not be emailed.

Explanation: The debug message does not appear because its level is lower than INFO. The remaining messages meet or exceed the configured logging level.

39.4 Creating Loggers

A logger is an object used to create log records. Named loggers help developers identify which part of an application produced a message. A project can have separate loggers for users, payments, reports, databases, or other modules. Using __name__ is common because it automatically uses the current module name.

Example: Create and use a named logger

# Import the logging module.
import logging

# Configure the root logging system.
logging.basicConfig(level=logging.INFO)

# Create a logger with a descriptive name.
logger = logging.getLogger("payment_service")

# Record a payment-processing event.
logger.info("Payment processing started.")

# Store a sample payment amount.
amount = 75.50

# Include the amount in a log message.
logger.info("Payment amount: $%.2f", amount)

# Record a successful result.
logger.info("Payment completed successfully.")
Output:
INFO:payment_service:Payment processing started.
INFO:payment_service:Payment amount: $75.50
INFO:payment_service:Payment completed successfully.

Explanation: The name payment_service appears in every log line. This makes it easier to identify which application component produced each message.

39.5 Handlers

A handler decides where log records are sent. A StreamHandler can send logs to the console, while a FileHandler writes them to a file. One logger can have multiple handlers. This allows an application to display important messages on the screen while saving more detailed information in a log file.

Example: Add a console handler

# Import the logging module.
import logging

# Create a named logger.
logger = logging.getLogger("inventory")

# Set the logger's minimum level.
logger.setLevel(logging.DEBUG)

# Create a handler that sends messages to the console.
console_handler = logging.StreamHandler()

# Allow INFO messages and above through this handler.
console_handler.setLevel(logging.INFO)

# Add the handler to the logger.
logger.addHandler(console_handler)

# This message is hidden by the handler.
logger.debug("Checking internal inventory values.")

# This message is displayed.
logger.info("Inventory check completed.")
Output:
Inventory check completed.

Explanation: The logger accepts debug messages, but the console handler accepts only INFO and higher. Therefore, the debug message is filtered before reaching the console.

39.6 Formatters

A formatter controls how each log record looks. It can include the date, time, level, logger name, filename, function name, line number, and message. Good formatting makes logs easier to scan and understand. Production logs should include enough information to identify when and where an event occurred.

Example: Add a custom formatter

# Import the logging module.
import logging

# Create a named logger.
logger = logging.getLogger("order_system")
logger.setLevel(logging.INFO)

# Create a console handler.
console_handler = logging.StreamHandler()

# Create a formatter with time, level, logger, and message.
formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)

# Attach the formatter to the handler.
console_handler.setFormatter(formatter)

# Attach the handler to the logger.
logger.addHandler(console_handler)

# Create a formatted log message.
logger.info("Order 205 was created.")
Output:
2026-07-19 15:30:10,245 | INFO | order_system | Order 205 was created.

Explanation: The exact date and time will be different when the code runs. The formatter places each piece of information in a consistent and readable order.

39.7 File Logging

File logging stores log messages permanently instead of showing them only in the terminal. This is useful when an application runs for a long time or when developers need to investigate an earlier event. A file handler can append new records or replace the previous file depending on the selected mode.

Example: Save log messages in a file

# Import the logging module.
import logging

# Configure logging to write into application.log.
logging.basicConfig(
    filename="application.log",
    filemode="a",
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s"
)

# Write log records into the file.
logging.info("Application started.")
logging.info("Customer data loaded.")
logging.warning("One customer record is incomplete.")
Output in application.log:
2026-07-19 15:40:12,105 | INFO | Application started.
2026-07-19 15:40:12,106 | INFO | Customer data loaded.
2026-07-19 15:40:12,106 | WARNING | One customer record is incomplete.

Explanation: The messages are written into application.log. The a file mode means new records are added to the end instead of deleting existing logs.

39.8 Rotating Logs

Log files can become extremely large when an application runs continuously. Rotating logs solve this problem by creating a new file after a size or time limit is reached. Older files can be renamed, compressed, or deleted. Python provides rotating handlers through the logging.handlers module.

Example: Rotate logs based on file size

# Import logging and the rotating handler.
import logging
from logging.handlers import RotatingFileHandler

# Create a named logger.
logger = logging.getLogger("rotating_example")
logger.setLevel(logging.INFO)

# Create a rotating file handler.
handler = RotatingFileHandler(
    "application.log",
    maxBytes=1000,
    backupCount=3
)

# Create a readable formatter.
formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(message)s"
)

# Apply the formatter.
handler.setFormatter(formatter)

# Add the handler to the logger.
logger.addHandler(handler)

# Create several log messages.
for number in range(1, 101):
    logger.info("Processing item number %s.", number)
Possible Output Files:
application.log
application.log.1
application.log.2
application.log.3

Explanation: When the current file reaches approximately 1,000 bytes, it is rotated. The handler keeps up to three backup files and removes older backups automatically.

39.9 Structured Logging

Structured logging records information in organized fields instead of placing everything inside one sentence. JSON is a common format because monitoring systems can search fields such as user ID, action, status, and duration. Structured logs are especially useful in large applications, web services, cloud systems, and distributed systems.

Example: Create a JSON-style log record

# Import the required modules.
import json
import logging
from datetime import datetime

# Configure simple console logging.
logging.basicConfig(level=logging.INFO, format="%(message)s")

# Create structured log information.
log_record = {
    "time": datetime.now().isoformat(),
    "level": "INFO",
    "event": "user_login",
    "user_id": 42,
    "status": "success"
}

# Convert the dictionary to JSON text.
message = json.dumps(log_record)

# Record the structured message.
logging.info(message)
Output:
{"time": "2026-07-19T15:45:20.123456", "level": "INFO", "event": "user_login", "user_id": 42, "status": "success"}

Explanation: Each important value has its own field. A log-processing system can search for all records where event is user_login or where status is success.

39.10 Exception Logging

Exception logging records errors together with information showing where they occurred. Inside an except block, logger.exception() automatically includes the traceback. A traceback lists the files, lines, and function calls involved in the error, making it much easier to find and correct the underlying problem.

Example: Log an exception with its traceback

# Import the logging module.
import logging

# Configure logging.
logging.basicConfig(
    level=logging.ERROR,
    format="%(levelname)s | %(message)s"
)

# Create a named logger.
logger = logging.getLogger("calculator")

try:
    # Attempt an invalid division.
    result = 10 / 0

except ZeroDivisionError:
    # Record the error and full traceback.
    logger.exception("The calculation failed.")
Output:
ERROR | The calculation failed.
Traceback (most recent call last):
  File "app.py", line 15, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero

Explanation: The error message is followed by a traceback. The traceback identifies the failing line and the specific exception type, ZeroDivisionError.

39.11 Logging Configuration

Larger applications should configure logging in one central location. Python’s logging.config module can load settings from a dictionary or configuration file. Central configuration prevents different modules from creating conflicting handlers and formats. It also makes it easier to change levels, destinations, and formats without editing every module.

Example: Configure logging with a dictionary

# Import logging and dictionary configuration.
import logging
import logging.config

# Create the logging configuration.
LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,

    "formatters": {
        "standard": {
            "format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
        }
    },

    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "standard"
        }
    },

    "loggers": {
        "my_application": {
            "handlers": ["console"],
            "level": "INFO",
            "propagate": False
        }
    }
}

# Apply the configuration.
logging.config.dictConfig(LOGGING_CONFIG)

# Get the configured logger.
logger = logging.getLogger("my_application")

# Create a log message.
logger.info("Logging configuration loaded.")
Output:
2026-07-19 15:50:30,520 | INFO | my_application | Logging configuration loaded.

Explanation: The dictionary defines a formatter, a console handler, and a named logger. The call to dictConfig() creates and connects these components.

39.12 Application Settings

Application settings control how a program behaves. Examples include an application name, debug mode, database address, upload limit, timeout, and logging level. Keeping settings separate from the main program code makes the application easier to change, test, and deploy. Settings can be stored in classes, dictionaries, environment variables, or files.

Example: Store settings in a class

# Create a class that stores application settings.
class Settings:
    # Store the application name.
    APP_NAME = "Task Manager"

    # Control whether debugging is active.
    DEBUG = True

    # Store the maximum number of tasks.
    MAX_TASKS = 100

    # Store the logging level.
    LOG_LEVEL = "INFO"


# Display the configured settings.
print(Settings.APP_NAME)
print(Settings.DEBUG)
print(Settings.MAX_TASKS)
print(Settings.LOG_LEVEL)
Output:
Task Manager
True
100
INFO

Explanation: Related settings are grouped inside one class. Other parts of the application can read them using names such as Settings.DEBUG and Settings.LOG_LEVEL.

39.13 Environment Variables

Environment variables are values stored outside the Python source code. They are useful for configuration that changes between computers or deployments. Examples include database addresses, application modes, API locations, and log levels. Python can read environment variables using os.getenv(), and a default value can be supplied when one is missing.

Example: Read environment variables

# Import the os module.
import os

# Read APP_NAME or use a default value.
app_name = os.getenv("APP_NAME", "Default Application")

# Read LOG_LEVEL or use INFO.
log_level = os.getenv("LOG_LEVEL", "INFO")

# Read DEBUG as text and convert it to a Boolean.
debug = os.getenv("DEBUG", "false").lower() == "true"

# Display the loaded settings.
print(f"Application: {app_name}")
print(f"Log level: {log_level}")
print(f"Debug mode: {debug}")
Example Environment Variables:
APP_NAME=Order Manager
LOG_LEVEL=DEBUG
DEBUG=true
Output:
Application: Order Manager
Log level: DEBUG
Debug mode: True

Explanation: The program loads values from the operating environment. The DEBUG value begins as text, so the code compares its lowercase value with true.

39.14 Configuration Files

Configuration files store settings in a separate file that can be edited without changing application code. Common formats include JSON, TOML, YAML, and INI. Python’s standard library can read JSON, TOML, and INI files. A program should handle missing files, invalid values, and parsing errors clearly.

Example: Read settings from a JSON file

{
    "app_name": "Book Manager",
    "debug": true,
    "max_books": 500,
    "log_level": "INFO"
}
# Import the json module.
import json

# Open the configuration file.
with open("config.json", "r", encoding="utf-8") as file:
    # Convert the JSON content to a Python dictionary.
    settings = json.load(file)

# Read and display individual settings.
print(settings["app_name"])
print(settings["debug"])
print(settings["max_books"])
print(settings["log_level"])
Output:
Book Manager
True
500
INFO

Explanation: The JSON object becomes a Python dictionary. The program can then access each setting by its key, such as app_name or max_books.

39.15 Development vs Production Configuration

Development and production environments usually need different settings. Development may enable detailed debugging and console logs, while production should use safer settings and fewer detailed messages. Production systems may also use different databases, domains, security options, and file locations. Separating environments prevents development settings from accidentally reaching real users.

Example: Select settings for each environment

# Import the os module.
import os

# Create the shared base settings.
class BaseConfig:
    APP_NAME = "Customer Portal"
    LOG_LEVEL = "INFO"


# Create development-specific settings.
class DevelopmentConfig(BaseConfig):
    DEBUG = True
    DATABASE_NAME = "customer_portal_dev"
    LOG_LEVEL = "DEBUG"


# Create production-specific settings.
class ProductionConfig(BaseConfig):
    DEBUG = False
    DATABASE_NAME = "customer_portal"
    LOG_LEVEL = "WARNING"


# Read the current environment.
environment = os.getenv("APP_ENV", "development")

# Select the correct settings class.
if environment == "production":
    config = ProductionConfig
else:
    config = DevelopmentConfig

# Display the selected settings.
print(config.APP_NAME)
print(config.DEBUG)
print(config.DATABASE_NAME)
print(config.LOG_LEVEL)
Development Output:
Customer Portal
True
customer_portal_dev
DEBUG

Explanation: When APP_ENV is not set to production, the program uses development settings. Production mode selects safer values and a higher logging threshold.

39.16 Secrets and Sensitive Data

Secrets are sensitive values such as passwords, private keys, database credentials, and API tokens. They should not be written directly into source code, committed to a public repository, or recorded in log files. Applications should load secrets from protected environment variables or a dedicated secret-management service and reveal as little information as possible.

Example: Load and safely display a secret

# Import the os module.
import os

# Read the secret API key.
api_key = os.getenv("API_KEY")

# Check whether the secret exists.
if not api_key:
    print("API_KEY is missing.")

else:
    # Show only the last four characters.
    hidden_key = "*" * max(0, len(api_key) - 4) + api_key[-4:]

    # Print the masked value.
    print(f"API key loaded: {hidden_key}")
Example Output:
API key loaded: ************7X9Q

Explanation: The program does not print the complete secret. Most characters are replaced with asterisks, reducing the risk of exposing the key in a terminal or log file.

39.17 Configuration Validation

Configuration validation checks that required settings exist and contain acceptable values before the application starts. Without validation, an invalid port, missing password, or unsupported log level may cause a confusing failure later. Early validation produces clearer error messages and prevents an application from starting with unsafe or incomplete settings.

Example: Validate application settings

# Create sample configuration values.
settings = {
    "app_name": "Inventory Manager",
    "port": 8080,
    "log_level": "INFO"
}

# List the supported logging levels.
valid_log_levels = {
    "DEBUG",
    "INFO",
    "WARNING",
    "ERROR",
    "CRITICAL"
}

# Confirm that the application name exists.
if not settings.get("app_name"):
    raise ValueError("app_name is required.")

# Confirm that the port is within the valid range.
port = settings.get("port")

if not isinstance(port, int) or not 1 <= port <= 65535:
    raise ValueError("port must be between 1 and 65535.")

# Confirm that the logging level is supported.
if settings.get("log_level") not in valid_log_levels:
    raise ValueError("log_level is invalid.")

# Confirm that validation succeeded.
print("Configuration is valid.")
Output:
Configuration is valid.

Explanation: The program checks every important setting before continuing. If any value is missing or invalid, it raises a clear ValueError.

39.18 Practical Logging Systems

A practical logging system usually sends messages to more than one destination. Developers may want readable console messages during development and permanent rotating files for later investigation. Each destination can have its own level and formatter. The logger should be configured only once so that duplicate handlers do not create repeated messages.

Example: Console and rotating file logging

# Import the required logging tools.
import logging
from logging.handlers import RotatingFileHandler

# Create the application logger.
logger = logging.getLogger("store_application")
logger.setLevel(logging.DEBUG)

# Prevent messages from also reaching the root logger.
logger.propagate = False

# Create a console handler.
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# Create a rotating file handler.
file_handler = RotatingFileHandler(
    "store.log",
    maxBytes=100000,
    backupCount=5,
    encoding="utf-8"
)

# Store detailed messages in the file.
file_handler.setLevel(logging.DEBUG)

# Create separate formatters.
console_formatter = logging.Formatter(
    "%(levelname)s | %(message)s"
)

file_formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | "
    "%(filename)s:%(lineno)d | %(message)s"
)

# Apply the formatters.
console_handler.setFormatter(console_formatter)
file_handler.setFormatter(file_formatter)

# Add handlers only when none exist.
if not logger.handlers:
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)

# Create several log records.
logger.debug("Detailed inventory values were loaded.")
logger.info("Store application started.")
logger.warning("One product has low inventory.")
Console Output:
INFO | Store application started.
WARNING | One product has low inventory.
File Output:
2026-07-19 16:10:20,401 | DEBUG | store_application | app.py:44 | Detailed inventory values were loaded.
2026-07-19 16:10:20,402 | INFO | store_application | app.py:45 | Store application started.
2026-07-19 16:10:20,402 | WARNING | store_application | app.py:46 | One product has low inventory.

Explanation: The console displays only INFO and higher messages. The file also records detailed debug information and automatically rotates when it grows too large.

39.19 Chapter Practice Exercises

These exercises help you practise selecting log levels, creating named loggers, formatting records, writing logs to files, loading environment variables, validating configuration values, and protecting sensitive information. Complete each exercise in a separate Python file, run it several times, and inspect both the terminal output and any generated log files.

Exercise 1: Basic logging

# Create a program that records:
# 1. An INFO message when the program starts.
# 2. A WARNING message for a missing optional file.
# 3. An ERROR message when an operation fails.

Exercise 2: Named logger

# Create a logger named "student_manager".
# Use it to record:
# 1. A student being added.
# 2. A student being removed.
# 3. An invalid student ID.

Exercise 3: File logging

# Write application messages into school.log.
# Include:
# - Date and time
# - Log level
# - Logger name
# - Message

Exercise 4: Environment settings

# Read these environment variables:
# APP_NAME
# APP_ENV
# LOG_LEVEL
#
# Use suitable default values when they are missing.

Exercise 5: Configuration validation

# Validate these settings:
# - app_name must not be empty.
# - port must be between 1 and 65535.
# - debug must be a Boolean.
# - log_level must be a standard logging level.
Possible Output:
All settings were loaded successfully.
Configuration is valid.
Logging system is ready.

Explanation: Your exact output depends on your solution. A correct solution should produce meaningful log records, use safe default settings, and clearly report invalid configuration values.

39.20 Chapter Project

In this project, you will build a small inventory application with a reusable settings loader and a practical logging system. The application will load configuration from environment variables, validate important values, write messages to the console and a rotating file, record errors with tracebacks, and avoid exposing secret information.

Step 1: Create the project structure

inventory_application/
│
├── app.py
├── config.py
├── logging_setup.py
├── inventory.py
└── logs/
    └── application.log

Step 2: Create config.py

# config.py

# Import the os module.
import os


# Create a settings class.
class Settings:
    # Read the application name.
    APP_NAME = os.getenv(
        "APP_NAME",
        "Beginner Inventory Application"
    )

    # Read the application environment.
    APP_ENV = os.getenv(
        "APP_ENV",
        "development"
    ).lower()

    # Read the log level.
    LOG_LEVEL = os.getenv(
        "LOG_LEVEL",
        "DEBUG"
    ).upper()

    # Read the inventory limit and convert it to an integer.
    MAX_ITEMS = int(
        os.getenv("MAX_ITEMS", "100")
    )

    # Read an optional secret API key.
    API_KEY = os.getenv("API_KEY")


# Define a function that validates the settings.
def validate_settings():
    # Store supported environments.
    valid_environments = {
        "development",
        "production"
    }

    # Store supported logging levels.
    valid_log_levels = {
        "DEBUG",
        "INFO",
        "WARNING",
        "ERROR",
        "CRITICAL"
    }

    # Confirm the application name exists.
    if not Settings.APP_NAME.strip():
        raise ValueError("APP_NAME cannot be empty.")

    # Confirm the environment is supported.
    if Settings.APP_ENV not in valid_environments:
        raise ValueError(
            "APP_ENV must be development or production."
        )

    # Confirm the log level is supported.
    if Settings.LOG_LEVEL not in valid_log_levels:
        raise ValueError("LOG_LEVEL is invalid.")

    # Confirm the inventory limit is positive.
    if Settings.MAX_ITEMS <= 0:
        raise ValueError(
            "MAX_ITEMS must be greater than zero."
        )

Step 3: Create logging_setup.py

# logging_setup.py

# Import the required modules.
import logging
from pathlib import Path
from logging.handlers import RotatingFileHandler

# Import the application settings.
from config import Settings


# Define a function that configures logging.
def configure_logging():
    # Create the logs folder when it does not exist.
    log_directory = Path("logs")
    log_directory.mkdir(exist_ok=True)

    # Create the application logger.
    logger = logging.getLogger("inventory_application")

    # Convert the text level to a logging value.
    logger.setLevel(
        getattr(logging, Settings.LOG_LEVEL)
    )

    # Prevent duplicate messages from the root logger.
    logger.propagate = False

    # Avoid adding the same handlers more than once.
    if logger.handlers:
        return logger

    # Create a console handler.
    console_handler = logging.StreamHandler()

    # Use a more detailed console level during development.
    if Settings.APP_ENV == "development":
        console_handler.setLevel(logging.DEBUG)
    else:
        console_handler.setLevel(logging.INFO)

    # Create a rotating file handler.
    file_handler = RotatingFileHandler(
        log_directory / "application.log",
        maxBytes=50000,
        backupCount=3,
        encoding="utf-8"
    )

    # Save all configured messages in the file.
    file_handler.setLevel(
        getattr(logging, Settings.LOG_LEVEL)
    )

    # Create a simple console formatter.
    console_formatter = logging.Formatter(
        "%(levelname)s | %(message)s"
    )

    # Create a detailed file formatter.
    file_formatter = logging.Formatter(
        "%(asctime)s | %(levelname)s | %(name)s | "
        "%(filename)s:%(lineno)d | %(message)s"
    )

    # Apply the formatters.
    console_handler.setFormatter(console_formatter)
    file_handler.setFormatter(file_formatter)

    # Add both handlers to the logger.
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)

    # Return the configured logger.
    return logger

Step 4: Create inventory.py

# inventory.py

# Create an inventory-management class.
class Inventory:
    # Prepare a new inventory.
    def __init__(self, max_items):
        # Store the maximum number of products.
        self.max_items = max_items

        # Store products in a dictionary.
        self.items = {}

    # Add a new product.
    def add_item(self, name, quantity):
        # Reject empty product names.
        if not name.strip():
            raise ValueError(
                "The product name cannot be empty."
            )

        # Reject negative quantities.
        if quantity < 0:
            raise ValueError(
                "The quantity cannot be negative."
            )

        # Prevent too many different products.
        if (
            name not in self.items
            and len(self.items) >= self.max_items
        ):
            raise ValueError(
                "The inventory limit was reached."
            )

        # Add or update the product quantity.
        self.items[name] = quantity

    # Remove a product.
    def remove_item(self, name):
        # Confirm that the product exists.
        if name not in self.items:
            raise KeyError(
                f"Product not found: {name}"
            )

        # Remove the product.
        del self.items[name]

    # Return a copy of all products.
    def get_items(self):
        return self.items.copy()

Step 5: Create app.py

# app.py

# Import the required project components.
from config import Settings, validate_settings
from logging_setup import configure_logging
from inventory import Inventory


# Define the main application function.
def main():
    try:
        # Validate all settings before starting.
        validate_settings()

        # Create the configured logger.
        logger = configure_logging()

        # Record the startup information.
        logger.info(
            "Starting %s.",
            Settings.APP_NAME
        )

        logger.info(
            "Environment: %s.",
            Settings.APP_ENV
        )

        logger.debug(
            "Maximum product types: %s.",
            Settings.MAX_ITEMS
        )

        # Report whether the secret exists without printing it.
        if Settings.API_KEY:
            logger.info("The API key was loaded securely.")
        else:
            logger.warning("No API key was configured.")

        # Create the inventory.
        inventory = Inventory(Settings.MAX_ITEMS)

        # Add sample products.
        inventory.add_item("Keyboard", 10)
        logger.info("Added product: Keyboard")

        inventory.add_item("Mouse", 25)
        logger.info("Added product: Mouse")

        inventory.add_item("Monitor", 8)
        logger.info("Added product: Monitor")

        # Display the current inventory.
        print("Current inventory:")

        for name, quantity in inventory.get_items().items():
            print(f"- {name}: {quantity}")

        # Demonstrate exception logging.
        try:
            inventory.remove_item("Printer")

        except KeyError:
            logger.exception(
                "A product could not be removed."
            )

        # Record normal application completion.
        logger.info("Application completed.")

    except Exception:
        # Create an emergency logger if startup fails.
        emergency_logger = configure_logging()

        # Record the complete startup error.
        emergency_logger.exception(
            "The application could not start."
        )


# Start the program.
if __name__ == "__main__":
    main()

Step 6: Set optional environment variables

APP_NAME=My Inventory Manager
APP_ENV=development
LOG_LEVEL=DEBUG
MAX_ITEMS=200
API_KEY=example-secret-value

Step 7: Run the application

python app.py
Console Output:
INFO | Starting My Inventory Manager.
INFO | Environment: development.
DEBUG | Maximum product types: 200.
INFO | The API key was loaded securely.
INFO | Added product: Keyboard
INFO | Added product: Mouse
INFO | Added product: Monitor

Current inventory:
- Keyboard: 10
- Mouse: 25
- Monitor: 8

ERROR | A product could not be removed.
Traceback (most recent call last):
  File "app.py", line 56, in main
    inventory.remove_item("Printer")
KeyError: 'Product not found: Printer'

INFO | Application completed.
Example File Output:
2026-07-19 16:30:10,100 | INFO | inventory_application | app.py:20 | Starting My Inventory Manager.
2026-07-19 16:30:10,101 | INFO | inventory_application | app.py:25 | Environment: development.
2026-07-19 16:30:10,101 | DEBUG | inventory_application | app.py:30 | Maximum product types: 200.
2026-07-19 16:30:10,102 | INFO | inventory_application | app.py:36 | The API key was loaded securely.
2026-07-19 16:30:10,103 | INFO | inventory_application | app.py:45 | Added product: Keyboard
2026-07-19 16:30:10,103 | INFO | inventory_application | app.py:48 | Added product: Mouse
2026-07-19 16:30:10,104 | INFO | inventory_application | app.py:51 | Added product: Monitor
2026-07-19 16:30:10,105 | ERROR | inventory_application | app.py:59 | A product could not be removed.
2026-07-19 16:30:10,106 | INFO | inventory_application | app.py:64 | Application completed.

Explanation: This project separates configuration, logging setup, inventory logic, and the main program into different modules. It validates settings before starting, reads environment variables, protects the API key, records normal events, captures exceptions with tracebacks, and stores detailed messages in a rotating log file.

End of Chapter 39: Logging and Application Configuration

Chapter 2: Development EnvironmentPrepare Your JavaScript Workspace

A modern course built to help learners study step by step with clarity, comfort, and confidence.