python LogoContext Managers

Context Managers are a feature in Python that allows you to allocate and release resources precisely when you want to. They are primarily used with the `with` statement, which guarantees that specific setup and teardown actions are performed before and after a block of code, respectively, regardless of how the block exits (e.g., normal completion or an unhandled exception).

Why use Context Managers?
- Resource Management: They simplify common resource management patterns like opening/closing files, acquiring/releasing locks, or connecting/disconnecting to databases.
- Guaranteed Cleanup: The `with` statement ensures that resources are properly cleaned up, even if an error occurs within the code block, preventing resource leaks.
- Error Handling: The `__exit__` method of a context manager can receive details about an exception, allowing it to handle or suppress the exception if necessary.

How they work:
A class becomes a context manager by implementing two special methods:
1. `__enter__(self)`: This method is executed when the `with` statement is entered. It should set up the context (e.g., open a file, acquire a lock). The value returned by `__enter__` (if any) is bound to the variable after the `as` keyword in the `with` statement.
2. `__exit__(self, exc_type, exc_val, exc_tb)`: This method is executed when the `with` block is exited. It performs the teardown operations (e.g., close the file, release the lock). It receives three arguments: `exc_type` (exception type), `exc_val` (exception value), and `exc_tb` (traceback object). If no exception occurred, all three will be `None`. If `__exit__` returns a truthy value, the exception is suppressed; otherwise, it is re-raised.

Common Use Cases:
- File I/O (`open()` function returns a context manager).
- Locking mechanisms (`threading.Lock`).
- Database connections.
- Temporary directories.
- Timing code execution.

Python's `contextlib` module provides convenient utilities for creating context managers, most notably the `@contextmanager` decorator, which allows you to define a context manager using a generator function.

Example Code

import time

 Example 1: Built-in Context Manager (File I/O)
 The 'open()' function returns a context manager for file handling.
print("--- Using built-in context manager (file) ---")
with open("my_log.txt", "w") as file_handle:
    file_handle.write("This is a log entry.\n")
    file_handle.write("Another entry.\n")
print("File operations completed. File is automatically closed.")

 Example 2: Custom Context Manager Class
 This class simulates managing a 'resource' that needs setup and teardown.
class DatabaseConnection:
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None

    def __enter__(self):
        print(f"[DB Manager] Entering context for '{self.db_name}'.")
         Simulate acquiring a resource (e.g., opening a database connection)
        self.connection = f"Connected to {self.db_name}"
        print(f"[DB Manager] {self.connection} established.")
        return self.connection   This value is bound to the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"[DB Manager] Exiting context for '{self.db_name}'.")
         Simulate releasing the resource (e.g., closing the database connection)
        if exc_type:
            print(f"[DB Manager] An exception occurred within the 'with' block:")
            print(f"[DB Manager] Type: {exc_type.__name__}, Value: {exc_val}")
             If you return True, the exception is suppressed and not re-raised.
             For this example, we'll let it re-raise to demonstrate error propagation.
             return True
        print(f"[DB Manager] Connection to {self.db_name} closed.")
        self.connection = None  Clean up state
        return False  Do not suppress exceptions by default

print("\n--- Using custom context manager (DatabaseConnection) ---")

 Scenario 1: Normal execution
print("\nScenario 1: Normal execution")
with DatabaseConnection("production_db") as db_conn:
    print(f"    Inside 'with' block. Current connection: {db_conn}")
    print("    Performing some database queries...")
    time.sleep(0.5)
print("After 'with' block for Scenario 1.\n")

 Scenario 2: Execution with an exception
print("Scenario 2: Execution with an exception")
try:
    with DatabaseConnection("reporting_db") as db_conn:
        print(f"    Inside 'with' block. Current connection: {db_conn}")
        print("    Performing some reporting queries...")
        time.sleep(0.5)
        raise ValueError("Simulated query error!")  An error occurs here
        print("    This line will not be reached.")
except ValueError as e:
    print(f"Caught expected error outside 'with' block: {e}")
print("After 'with' block for Scenario 2 (after exception handling).")