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.
Main reading content
Chapter 28: Context Managers
A complete beginner-friendly guide to Python context managers, the with statement, automatic resource management, custom context managers, contextlib, exception handling, output redirection, and asynchronous contexts.
Chapter 28 Topics
- 28.1 Introduction to Context Managers
- 28.2 Resource Management
- 28.3 The
withStatement - 28.4
__enter__() - 28.5
__exit__() - 28.6 Creating Custom Context Managers
- 28.7 The
contextlibModule - 28.8 The
contextmanagerDecorator - 28.9 Nested Context Managers
- 28.10 Multiple Context Managers
- 28.11 Suppressing Exceptions
- 28.12 Redirecting Output
- 28.13 Asynchronous Context Managers
- 28.14 Practical Context Manager Applications
- 28.15 Chapter Practice Exercises
- 28.16 Chapter Mini Project
28.1 Introduction to Context Managers
```A context manager is a Python object that prepares a resource before a block of code runs and performs cleanup after the block finishes. Context managers are commonly used with files, database connections, locks, temporary folders, network connections, and other resources that must be closed or released correctly.
The most familiar context manager is a file opened with the with statement. Python opens the file when the block begins and closes it automatically when the block ends. The cleanup still happens if an error occurs inside the block, which makes context managers safer than manual cleanup.
Example
# Open a file using a context manager
```
with open("message.txt", "w") as file:
file.write("Hello from Python!")
# The file is automatically closed here
print("The file operation is complete.")
```
Output
The file operation is complete.
File Content
Hello from Python!
Output Explanation
The file is opened before the indented block begins. The text is written while the file is available. When execution leaves the block, Python automatically closes the file, even though the program does not call file.close() manually.
28.2 Resource Management
```Resource management means safely controlling resources that a program temporarily uses. Examples include files, database connections, network sockets, threads, locks, memory buffers, and temporary configuration changes. Many resources must be released when the program finishes using them.
Failing to release a resource can cause memory waste, locked files, unavailable database connections, incomplete data, or application errors. A context manager places setup and cleanup logic in one reusable object so the programmer is less likely to forget an important cleanup step.
Example Without a Context Manager
# Open the file manually
```
file = open("notes.txt", "w")
try:
file.write("Learning resource management.")
finally:
# The finally block ensures that the file closes
file.close()
print("File closed:", file.closed)
```
Output
File closed: True
Example Using a Context Manager
with open("notes.txt", "w") as file:
file.write("Learning resource management.")
```
print("File closed:", file.closed)
```
Output
File closed: True
Output Explanation
Both examples close the file safely. The first requires a try and finally structure. The second uses with, which is shorter, easier to read, and automatically performs the cleanup.
28.3 The with Statement
```
The with statement begins a managed block of code. Python asks the context manager to prepare the resource, makes the resource available inside the block, and then performs cleanup when the block ends.
The optional as keyword stores the value returned by the context manager. For files, this value is the file object. The indented block is called the context. When execution leaves that context, cleanup occurs automatically.
General Syntax
with context_manager as resource:
# Use the managed resource
statements
Example
# Create a file and write several lines
```
with open("students.txt", "w") as student_file:
student_file.write("Ali\n")
student_file.write("Sara\n")
student_file.write("Michael\n")
# Read the file using another context manager
with open("students.txt", "r") as student_file:
content = student_file.read()
print(content)
```
Output
Ali
```
Sara
Michael
```
Output Explanation
The first with block opens the file for writing and closes it afterward. The second block opens the same file for reading. Each file object is managed separately and closed automatically when its block finishes.
28.4 __enter__()
```
The __enter__() method runs when execution enters a with block. It is responsible for preparing the resource or environment. It may open a connection, create a temporary object, acquire a lock, start a timer, or perform another setup operation.
The value returned by __enter__() is assigned to the variable after the as keyword. A context manager often returns self, but it may return another object, such as an opened file or connection.
Example
class GreetingContext:
def __enter__(self):
print("Entering the context")
# This value is assigned after the as keyword
return "Welcome to the managed block"
def __exit__(self, exception_type, exception_value, traceback):
print("Leaving the context")
```
with GreetingContext() as message:
print(message)
```
Output
Entering the context
```
Welcome to the managed block
Leaving the context
```
Output Explanation
Python calls __enter__() before running the block. The returned string is stored in message. After the block finishes, Python calls __exit__().
28.5 __exit__()
```
The __exit__() method runs when execution leaves a with block. It performs cleanup and receives information about any exception that occurred inside the block. Its parameters commonly represent the exception type, exception value, and traceback.
When no exception occurs, all three exception values are None. If __exit__() returns True, Python treats the exception as handled and suppresses it. Returning False or None allows the exception to continue normally.
Example
class ErrorReporter:
def __enter__(self):
print("Context started")
return self
def __exit__(self, exception_type, exception_value, traceback):
print("Context finished")
if exception_type is not None:
print("Exception type:", exception_type.__name__)
print("Exception message:", exception_value)
# False means the exception is not suppressed
return False
```
try:
with ErrorReporter():
print("Running the managed code")
result = 10 / 0
except ZeroDivisionError:
print("The outer code caught the error.")
```
Output
Context started
```
Running the managed code
Context finished
Exception type: ZeroDivisionError
Exception message: division by zero
The outer code caught the error.
```
Output Explanation
The division causes an exception. Before the exception leaves the with block, Python calls __exit__() and provides the error information. Because the method returns False, the outer except block receives the error.
28.6 Creating Custom Context Managers
```
A class becomes a context manager when it defines both __enter__() and __exit__(). The class can store information in instance attributes and manage a resource throughout the life of the with block.
Custom context managers are useful when a program repeatedly performs the same setup and cleanup steps. Keeping those steps inside one class makes the main program shorter and reduces the risk of forgetting cleanup.
Example: Managed Text File
class ManagedTextFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print("Opening:", self.filename)
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exception_type, exception_value, traceback):
if self.file is not None:
self.file.close()
print("Closing:", self.filename)
# Do not suppress exceptions
return False
```
with ManagedTextFile("course.txt", "w") as file:
file.write("Python Context Managers")
print("File operation completed.")
```
Output
Opening: course.txt
```
Closing: course.txt
File operation completed.
```
File Content
Python Context Managers
Output Explanation
The custom class opens the file inside __enter__() and returns it. The block writes the text. When the block ends, __exit__() closes the file and displays the closing message.
28.7 The contextlib Module
```
Python's contextlib module provides utilities for creating and working with context managers. It includes contextmanager, closing, suppress, redirect_stdout, redirect_stderr, nullcontext, ExitStack, and asynchronous alternatives.
These tools can reduce the amount of code needed for common resource-management tasks. Instead of always writing a class with __enter__() and __exit__(), programmers can sometimes use a function or an existing helper from contextlib.
Example Using closing
from contextlib import closing
```
class SimpleResource:
def use(self):
print("The resource is being used.")
```
def close(self):
print("The resource is now closed.")
```
# closing() calls the resource's close() method automatically
with closing(SimpleResource()) as resource:
resource.use()
```
Output
The resource is being used.
```
The resource is now closed.
```
Output Explanation
The SimpleResource class has a close() method but is not itself a context manager. The closing() helper wraps it and calls its close() method when the block finishes.
28.8 The contextmanager Decorator
```
The contextmanager decorator converts a generator function into a context manager. Code before yield performs setup. The yielded value is assigned after the as keyword. Code after yield performs cleanup.
A try and finally structure is commonly used so cleanup happens even when an exception occurs. This method is often shorter than creating a complete context-manager class.
Example
from contextlib import contextmanager
```
@contextmanager
def managed_message():
print("Preparing the context")
```
try:
yield "Resource is ready"
finally:
print("Cleaning up the context")
```
with managed_message() as message:
print(message)
print("Working inside the block")
```
Output
Preparing the context
```
Resource is ready
Working inside the block
Cleaning up the context
```
Output Explanation
The setup code runs before yield. The yielded string is stored in message. After the block finishes, execution resumes after yield, and the cleanup message is displayed.
Example: Managed File Function
from contextlib import contextmanager
```
@contextmanager
def open_text_file(filename, mode):
file = open(filename, mode)
```
try:
yield file
finally:
file.close()
```
with open_text_file("example.txt", "w") as file:
file.write("Created with contextmanager.")
28.9 Nested Context Managers
```A context manager can be placed inside another context manager. This is called nesting. The inner context begins after the outer context has already started. When execution finishes, the inner context closes first and the outer context closes afterward.
Nested context managers are useful when one managed resource depends on another. For example, a program may open one file for reading and then open another file for writing inside the first context.
Example
# Create the original file
```
with open("original.txt", "w") as file:
file.write("This text will be copied.")
# Open one file inside another context
with open("original.txt", "r") as source_file:
with open("copy.txt", "w") as destination_file:
content = source_file.read()
destination_file.write(content)
print("The file was copied.")
```
Output
The file was copied.
Content of copy.txt
This text will be copied.
Output Explanation
The source file is opened first. The destination file is opened inside the source-file context. When the inner block ends, the destination file closes first. The source file closes when the outer block ends.
```28.10 Multiple Context Managers
```
Python allows multiple context managers in one with statement. The managers are separated by commas. This can make code shorter and less deeply indented than using several nested with blocks.
The context managers are entered from left to right. When the block ends, they exit in reverse order. This means the last resource opened is the first resource closed.
Example
# Create a source file
```
with open("numbers.txt", "w") as file:
file.write("10\n20\n30\n")
# Use two context managers in one statement
with open("numbers.txt", "r") as source,
open("number-copy.txt", "w") as destination:
```
for line in source:
destination.write(line)
```
print("All numbers were copied.")
```
Output
All numbers were copied.
Content of number-copy.txt
10
```
20
30
```
Output Explanation
Both files are managed by the same with statement. The source file provides each line, and the destination file receives it. Both files close automatically when the block ends.
28.11 Suppressing Exceptions
```
Exception suppression means intentionally preventing a selected exception from stopping the program. A custom context manager can suppress an exception by returning True from __exit__(). The contextlib.suppress() helper provides a shorter approach.
Suppression should be used only when the selected error is expected and safely ignored. Suppressing broad exceptions can hide important problems and make debugging difficult.
Example Using contextlib.suppress()
from contextlib import suppress
```
print("Program started")
# Ignore FileNotFoundError only
with suppress(FileNotFoundError):
with open("missing-file.txt", "r") as file:
print(file.read())
print("Program continued safely")
```
Output
Program started
```
Program continued safely
```
Output Explanation
Opening the missing file raises FileNotFoundError. The suppress() context manager handles that specific exception, so the program continues after the block.
Example with a Custom Context Manager
class IgnoreZeroDivision:
def __enter__(self):
print("Calculation started")
def __exit__(self, exception_type, exception_value, traceback):
if exception_type is ZeroDivisionError:
print("Division by zero was ignored.")
return True
return False
```
with IgnoreZeroDivision():
result = 10 / 0
print("The program is still running.")
```
Output
Calculation started
```
Division by zero was ignored.
The program is still running.
28.12 Redirecting Output
```
Output redirection temporarily sends printed output somewhere other than the normal terminal. The contextlib.redirect_stdout() context manager can send print() output to a file or an in-memory text buffer.
This is useful for saving reports, capturing output from existing functions, testing printed messages, or creating log files. When the context ends, normal terminal output is restored automatically.
Example: Redirect Output to a File
from contextlib import redirect_stdout
```
with open("report.txt", "w") as report_file:
with redirect_stdout(report_file):
print("Student Report")
print("----------------")
print("Ali: 85")
print("Sara: 92")
print("Michael: 78")
print("The report was saved.")
```
Terminal Output
The report was saved.
Content of report.txt
Student Report
```
---
Ali: 85
Sara: 92
Michael: 78
```
Example: Capture Output in Memory
from contextlib import redirect_stdout
```
from io import StringIO
output_buffer = StringIO()
with redirect_stdout(output_buffer):
print("Captured message")
print("Another captured line")
captured_text = output_buffer.getvalue()
print("Stored output:")
print(captured_text)
```
Output
Stored output:
```
Captured message
Another captured line
```
Output Explanation
The first example sends printed report lines to a file. The second sends them to a StringIO object in memory. After each managed block, normal printing returns automatically.
28.13 Asynchronous Context Managers
```
An asynchronous context manager manages resources used by asynchronous code. It defines __aenter__() and __aexit__() instead of the regular context-manager methods. It is used with the async with statement.
Asynchronous context managers are useful when setup or cleanup must wait for an asynchronous operation, such as opening a network connection, communicating with a server, or closing an asynchronous database session.
Example
import asyncio
```
class AsyncConnection:
async def **aenter**(self):
print("Opening the asynchronous connection...")
```
# Simulate an asynchronous setup operation
await asyncio.sleep(1)
print("Connection opened")
return self
async def send(self, message):
print("Sending:", message)
# Simulate an asynchronous operation
await asyncio.sleep(1)
print("Message sent")
async def __aexit__(self, exception_type, exception_value, traceback):
print("Closing the asynchronous connection...")
# Simulate asynchronous cleanup
await asyncio.sleep(1)
print("Connection closed")
```
async def main():
async with AsyncConnection() as connection:
await connection.send("Hello server")
asyncio.run(main())
```
Output
Opening the asynchronous connection...
```
Connection opened
Sending: Hello server
Message sent
Closing the asynchronous connection...
Connection closed
```
Output Explanation
The async with statement waits for __aenter__() to complete. The message is then sent asynchronously. When the block ends, Python waits for __aexit__() to finish its cleanup.
28.14 Practical Context Manager Applications
```Context managers are used for files, database transactions, timers, temporary directories, locks, network connections, redirected output, testing environments, and temporary configuration changes. Any operation with a clear setup and cleanup stage may benefit from a context manager.
In this example, a custom context manager measures how long a block of code takes. The timer begins when the block starts and displays the elapsed time after the block finishes.
Example: Block Timer
import time
```
class BlockTimer:
def **init**(self, label):
self.label = label
self.start_time = None
```
def __enter__(self):
print(self.label, "started")
self.start_time = time.perf_counter()
return self
def __exit__(self, exception_type, exception_value, traceback):
end_time = time.perf_counter()
duration = end_time - self.start_time
print(self.label, "finished")
print("Duration:", duration, "seconds")
return False
```
with BlockTimer("Large calculation"):
total = 0
```
for number in range(1, 1000001):
total += number
```
print("Total:", total)
```
Example Output
Large calculation started
```
Large calculation finished
Duration: 0.0583174000001 seconds
Total: 500000500000
```
Output Explanation
The timer records the starting time in __enter__(). After the calculation ends, __exit__() records the finishing time and calculates the difference. The exact duration will vary between computers.
Example: Temporary Setting
class TemporarySetting:
def __init__(self, settings, key, temporary_value):
self.settings = settings
self.key = key
self.temporary_value = temporary_value
self.original_value = None
def __enter__(self):
self.original_value = self.settings.get(self.key)
self.settings[self.key] = self.temporary_value
return self.settings
def __exit__(self, exception_type, exception_value, traceback):
self.settings[self.key] = self.original_value
```
application_settings = {
"debug": False
}
print("Before:", application_settings)
with TemporarySetting(application_settings, "debug", True):
print("Inside:", application_settings)
print("After:", application_settings)
```
Output
Before: {'debug': False}
```
Inside: {'debug': True}
After: {'debug': False}
28.15 Chapter Practice Exercises
```
These exercises help you practise resource management, the with statement, context-manager methods, contextlib, exception suppression, output redirection, asynchronous contexts, and practical managed resources.
- Open a text file with
withand write a greeting. - Read a file using a context manager.
- Confirm that a file is closed after its
withblock. - Copy one text file into another using nested context managers.
- Copy a file using two managers in one
withstatement. - Create a class with
__enter__()and__exit__(). - Return a message from
__enter__(). - Display exception information inside
__exit__(). - Create a context manager that opens and closes a custom resource.
- Create a timer context manager.
- Create a context manager that counts operations inside a block.
- Create a context manager using
@contextmanager. - Create a generator-based file context manager.
- Use
contextlib.closing()with an object that hasclose(). - Use
contextlib.suppress()to ignoreFileNotFoundError. - Build a custom context manager that suppresses
ZeroDivisionError. - Redirect printed output to a text file.
- Capture printed output with
StringIO. - Create a temporary-setting context manager.
- Create a context manager that temporarily changes the current folder.
- Create an asynchronous context manager.
- Create a context manager that logs when a task starts and finishes.
- Create a context manager that safely commits or rolls back a transaction.
Practice Example: Temporary List Item
class TemporaryItem:
def __init__(self, collection, item):
self.collection = collection
self.item = item
def __enter__(self):
self.collection.append(self.item)
return self.collection
def __exit__(self, exception_type, exception_value, traceback):
self.collection.remove(self.item)
```
shopping_list = ["Milk", "Bread"]
print("Before:", shopping_list)
with TemporaryItem(shopping_list, "Apples") as current_list:
print("Inside:", current_list)
print("After:", shopping_list)
```
Output
Before: ['Milk', 'Bread']
```
Inside: ['Milk', 'Bread', 'Apples']
After: ['Milk', 'Bread']
```
Output Explanation
The context manager adds Apples when the block begins. The item remains available inside the block. When the block ends, __exit__() removes it and restores the original list.
28.16 Chapter Mini Project
```Project: Context-Managed Transaction and Report System
In this mini project, you will create a small transaction system that safely manages data changes. A transaction context manager creates a backup before modifications begin. If all operations succeed, the changes are committed. If an exception occurs, the original data is restored.
A second context manager measures execution time, and output redirection saves the transaction report to a file. This project combines custom context managers, __enter__(), __exit__(), exception handling, backups, timing, file management, and contextlib.redirect_stdout().
Complete Program
import copy
```
import time
from contextlib import redirect_stdout
class TransactionManager:
def **init**(self, accounts):
self.accounts = accounts
self.backup = None
self.committed = False
```
def __enter__(self):
# Create a complete backup before changes begin
self.backup = copy.deepcopy(self.accounts)
print("Transaction started.")
print("A backup of the account data was created.")
return self
def commit(self):
# Mark the transaction as successful
self.committed = True
print("Transaction marked for commit.")
def __exit__(self, exception_type, exception_value, traceback):
if exception_type is not None:
# Restore the backup if an error occurred
self.accounts.clear()
self.accounts.update(self.backup)
print("Transaction error:", exception_value)
print("All changes were rolled back.")
# Suppress the expected transaction error
return True
if not self.committed:
# Restore the backup if commit() was not called
self.accounts.clear()
self.accounts.update(self.backup)
print("Transaction was not committed.")
print("All changes were rolled back.")
else:
print("Transaction committed successfully.")
return False
```
class OperationTimer:
def **init**(self, operation_name):
self.operation_name = operation_name
self.start_time = None
```
def __enter__(self):
print(self.operation_name, "started.")
self.start_time = time.perf_counter()
return self
def __exit__(self, exception_type, exception_value, traceback):
end_time = time.perf_counter()
duration = end_time - self.start_time
print(self.operation_name, "finished.")
print("Execution time:", round(duration, 6), "seconds")
return False
```
def transfer_money(accounts, sender, receiver, amount):
# Validate the requested amount
if not isinstance(amount, (int, float)):
raise TypeError("The transfer amount must be numeric.")
```
if amount <= 0:
raise ValueError("The transfer amount must be positive.")
# Validate the accounts
if sender not in accounts:
raise KeyError(f"Sender account '{sender}' was not found.")
if receiver not in accounts:
raise KeyError(f"Receiver account '{receiver}' was not found.")
# Check the sender's balance
if accounts[sender] < amount:
raise ValueError("The sender does not have enough money.")
# Perform the transfer
accounts[sender] -= amount
accounts[receiver] += amount
print(f"Transferred ${amount:.2f} from {sender} to {receiver}.")
```
def display_accounts(accounts):
print()
print("Current Account Balances")
print("--------------------------------")
```
for account_name, balance in accounts.items():
print(f"{account_name}: ${balance:.2f}")
```
accounts = {
"Sara": 1500.00,
"Michael": 800.00,
"Ali": 1200.00
}
with open("transaction-report.txt", "w") as report_file:
with redirect_stdout(report_file):
print("BANK TRANSACTION REPORT")
print("================================")
```
display_accounts(accounts)
print()
print("Successful Transaction")
print("--------------------------------")
with OperationTimer("Successful transfer"):
with TransactionManager(accounts) as transaction:
transfer_money(
accounts,
sender="Sara",
receiver="Michael",
amount=300.00
)
transaction.commit()
display_accounts(accounts)
print()
print("Failed Transaction")
print("--------------------------------")
with OperationTimer("Failed transfer"):
with TransactionManager(accounts) as transaction:
transfer_money(
accounts,
sender="Michael",
receiver="Ali",
amount=5000.00
)
transaction.commit()
display_accounts(accounts)
print()
print("Uncommitted Transaction")
print("--------------------------------")
with TransactionManager(accounts):
transfer_money(
accounts,
sender="Ali",
receiver="Sara",
amount=100.00
)
# commit() is intentionally not called
display_accounts(accounts)
print()
print("End of report.")
```
print("The transaction report was created.")
print("Final balances:")
for account_name, balance in accounts.items():
print(f"{account_name}: ${balance:.2f}")
```
Terminal Output
The transaction report was created.
```
Final balances:
Sara: $1200.00
Michael: $1100.00
Ali: $1200.00
```
Content of transaction-report.txt
BANK TRANSACTION REPORT
```
================================
## Current Account Balances
Sara: $1500.00
Michael: $800.00
Ali: $1200.00
## Successful Transaction
Successful transfer started.
Transaction started.
A backup of the account data was created.
Transferred $300.00 from Sara to Michael.
Transaction marked for commit.
Transaction committed successfully.
Successful transfer finished.
Execution time: 0.000041 seconds
## Current Account Balances
Sara: $1200.00
Michael: $1100.00
Ali: $1200.00
## Failed Transaction
Failed transfer started.
Transaction started.
A backup of the account data was created.
Transaction error: The sender does not have enough money.
All changes were rolled back.
Failed transfer finished.
Execution time: 0.000026 seconds
## Current Account Balances
Sara: $1200.00
Michael: $1100.00
Ali: $1200.00
## Uncommitted Transaction
Transaction started.
A backup of the account data was created.
Transferred $100.00 from Ali to Sara.
Transaction was not committed.
All changes were rolled back.
## Current Account Balances
Sara: $1200.00
Michael: $1100.00
Ali: $1200.00
End of report.
```
Project Explanation
The TransactionManager receives the shared account dictionary. When the context starts, __enter__() creates a deep copy of the account data. This backup can restore the original balances if the transaction fails.
The commit() method marks the transaction as successful. When the block ends without an exception and commit() was called, __exit__() keeps the modified balances.
If an exception occurs, __exit__() clears the modified dictionary and restores the backup. It returns True, so the expected transaction exception is handled without stopping the complete report.
If no exception occurs but commit() is not called, the context manager also restores the backup. This prevents incomplete or accidental changes from remaining in the account data.
The OperationTimer context manager records the time before each transfer and calculates the duration afterward. It works independently from the transaction manager, demonstrating how context managers can be nested.
The redirect_stdout() context manager sends all printed report information into transaction-report.txt. After that context ends, normal terminal output is restored automatically.
How to Run the Mini Project
- Open Visual Studio Code, IDLE, PyCharm, or another Python editor.
- Create a new file named
context_transaction_system.py. - Copy the complete project code into the file.
- Save the file.
- Open a terminal in the folder containing the file.
- Run
python context_transaction_system.py. - On some computers, run
python3 context_transaction_system.py. - Review the final balances displayed in the terminal.
- Open
transaction-report.txtin the same folder. - Review the successful, failed, and uncommitted transactions.
- Change the account balances and transfer amounts.
- Run the program again and compare the report.
Project Challenges
- Ask the user to enter the sender, receiver, and amount.
- Add deposits and withdrawals.
- Save account balances to a JSON file.
- Load account balances from a file when the program starts.
- Create a separate log file for failed transactions.
- Add transaction dates and times.
- Add a unique transaction number.
- Add a transfer fee.
- Prevent transfers between the same account.
- Create a database-style transaction manager.
- Add a context manager for temporarily locking an account.
- Add an asynchronous version of the transaction manager.
- Use
contextlib.ExitStackto manage several report files. - Create reports for individual account holders.
A modern course built to help learners study step by step with clarity, comfort, and confidence.