Lab 14: Context Managers & Protocols

Objective

Master Python's data model: __dunder__ methods, custom context managers, iterator protocol, descriptor protocol, and __slots__ for memory efficiency.

Time

30 minutes

Prerequisites

  • Lab 01 (Advanced OOP), Lab 07 (Type Hints)

Tools

  • Docker image: zchencow/innozverse-python:latest


Lab Instructions

Step 1: Context Manager Protocol

docker run --rm zchencow/innozverse-python:latest python3 -c "
from contextlib import contextmanager, asynccontextmanager, suppress
import time, threading

# Class-based context manager
class Timer:
    def __init__(self, name: str = ''):
        self.name = name; self.elapsed = 0.0

    def __enter__(self) -> Timer:
        self._start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        self.elapsed = time.perf_counter() - self._start
        label = f'[{self.name}] ' if self.name else ''
        print(f'{label}Elapsed: {self.elapsed*1000:.2f}ms')
        return False  # don't suppress exceptions

with Timer('sort') as t:
    data = sorted(range(10_000), reverse=True)
print(f'Timer accessible after: {t.elapsed*1000:.2f}ms')

# Generator-based context manager
@contextmanager
def transaction(conn_name: str):
    print(f'  BEGIN TRANSACTION [{conn_name}]')
    try:
        yield conn_name
        print(f'  COMMIT [{conn_name}]')
    except Exception as e:
        print(f'  ROLLBACK [{conn_name}]: {e}')
        raise
    finally:
        print(f'  CONNECTION CLOSED [{conn_name}]')

print()
print('=== Successful transaction ===')
with transaction('db-main') as conn:
    print(f'  Executing on: {conn}')
    print(f'  INSERT INTO orders ...')

print()
print('=== Failed transaction ===')
try:
    with transaction('db-replica') as conn:
        print(f'  UPDATE products ...')
        raise ValueError('constraint violation')
except ValueError:
    pass

# Nested context managers
print()
print('=== Nested CMs ===')
with Timer('outer'), Timer('inner-a'):
    x = sum(range(100_000))

# suppress — silently ignore specific errors
print()
with suppress(FileNotFoundError):
    open('/nonexistent/file.txt')
print('FileNotFoundError suppressed cleanly')

# Reentrant context manager
class BulkWriter:
    def __init__(self, name: str):
        self.name = name; self._depth = 0; self._buffer = []

    def __enter__(self) -> BulkWriter:
        self._depth += 1
        if self._depth == 1: print(f'  [{self.name}] Opened')
        return self

    def __exit__(self, *args) -> bool:
        self._depth -= 1
        if self._depth == 0:
            print(f'  [{self.name}] Flushed {len(self._buffer)} items')
        return False

    def write(self, item: str) -> None:
        self._buffer.append(item)

with BulkWriter('products') as w:
    w.write('Surface Pro')
    with w:  # reentrant — same object
        w.write('Surface Pen')
    w.write('Office 365')
"

💡 __exit__ returning True suppresses exceptions — the with block's exception is silently ignored. Return False (or None) to let exceptions propagate. This is how suppress() works internally. Always be explicit about your intent.

📸 Verified Output:


Step 2: Iterator Protocol & Custom Iterables

📸 Verified Output:


Steps 3–8: Descriptor Protocol, __slots__, Comparison Operators, __repr__, __hash__, Capstone

📸 Verified Output:


Summary

Dunder
Protocol
When to use

__enter__ / __exit__

Context manager

Resource management

__iter__ / __next__

Iterator

Custom iteration

__get__ / __set__ / __set_name__

Descriptor

Reusable validation

__eq__ / __lt__ + @total_ordering

Comparison

Sorting, equality

__hash__

Hashable

Use in sets/dict keys

__repr__ / __str__

String repr

Debugging / display

__slots__

Memory

High-volume objects

__len__ / __bool__

Container

len() / if obj:

Further Reading

Last updated