# How the Async Context Manager Pattern Works with NotebookLMClient

> Discover how NotebookLMClient uses the async context manager pattern. Ensure automatic connection management and transparent authentication refreshes with async with blocks, preventing resource leaks.

- Repository: [Teng Lin/notebooklm-py](https://github.com/teng-lin/notebooklm-py)
- Tags: deep-dive
- Published: 2026-03-09

---

**NotebookLMClient implements the async context manager pattern to automatically open HTTP connections when entering an `async with` block and guarantee cleanup via `__aexit__`, preventing resource leaks while handling authentication refreshes transparently.**

The `teng-lin/notebooklm-py` library provides a Python interface to Notebook LM's API, with `NotebookLMClient` serving as the primary entry point. Understanding how the async context manager pattern manages the client's lifecycle is essential for writing robust asynchronous code that handles network resources efficiently.

## Factory Initialization Without Network I/O

The entry point `NotebookLMClient.from_storage()` creates a client instance without immediately opening network connections. This classmethod reads stored authentication tokens from Playwright's storage state and returns a initialized client that remains dormant until the async context is entered.

```python
client = await NotebookLMClient.from_storage()   # reads stored auth tokens

```

According to the source code in [`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py) (lines 19-43), this factory builds an `AuthTokens` object from the storage file but defers HTTP client creation until `__aenter__` is called. This lazy initialization ensures that network resources are allocated only when actually needed.

## Entering the Context with __aenter__

When execution enters the `async with` block, Python invokes `NotebookLMClient.__aenter__`. This method delegates to the internal `ClientCore.open()` method to establish the HTTP session.

```python
async def __aenter__(self) -> "NotebookLMClient":
    logger.debug("Opening NotebookLM client")
    await self._core.open()
    return self

```

As implemented in [`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py) (lines 101-108), `__aenter__` returns the client instance after initialization. The heavy lifting occurs in `ClientCore.open()` at [`src/notebooklm/_core.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py) (lines 24-45), which instantiates an `httpx.AsyncClient` with proper headers—including authentication cookies, CSRF tokens, and timeout configuration—making the client ready for RPC calls.

## Guaranteed Cleanup via __aexit__

When the `async with` block exits—whether normally or via an exception—Python automatically calls `NotebookLMClient.__aexit__`. This ensures the underlying HTTP client closes properly, returning sockets to the operating system.

```python
async def __aexit__(self, exc_type, exc_val, exc_tb):
    logger.debug("Closing NotebookLM client")
    await self._core.close()

```

The implementation in [`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py) (lines 109-115) forwards the close operation to `ClientCore.close()`, which asynchronously shuts down the `httpx.AsyncClient` and releases all references. This pattern eliminates connection leaks even when exceptions occur during API operations.

## Benefits of the Async Context Manager Pattern

Using `NotebookLMClient` within an async context manager provides three critical advantages:

- **Resource Safety**: The HTTP session lifespan is strictly bound to the `async with` block, preventing orphaned connections and file descriptor exhaustion.
- **Automatic Token Refresh**: `ClientCore` accepts a refresh callback (`NotebookLMClient.refresh_auth`). When RPC calls encounter authentication errors, the core transparently refreshes tokens and retries requests while maintaining the active context.
- **Shared State**: All sub-clients (`NotebooksAPI`, `SourcesAPI`, `ChatAPI`) receive the same `ClientCore` instance during construction, sharing a single HTTP connection pool and refresh logic throughout the context lifetime.

## Practical Implementation Examples

### Basic Notebook Listing

This example demonstrates the complete lifecycle: connection establishment, API call, and automatic cleanup.

```python
import asyncio
from notebooklm import NotebookLMClient

async def list_notebooks():
    async with await NotebookLMClient.from_storage() as client:
        notebooks = await client.notebooks.list()
        for nb in notebooks:
            print(nb["id"], nb["title"])

asyncio.run(list_notebooks())

```

The `await NotebookLMClient.from_storage()` call initializes the client; the `async with` block opens the connection, executes the RPC, and automatically closes resources upon exit.

### Custom Authentication and Timeouts

For scenarios requiring manual authentication or custom timeout values:

```python
from notebooklm import NotebookLMClient
from notebooklm.auth import AuthTokens

async def use_custom_auth():
    auth = AuthTokens(cookies="...", csrf_token="...", session_id="...")
    async with NotebookLMClient(auth, timeout=60.0) as client:
        await client.sources.add_url("nb123", "https://example.com")

```

### Handling Token Refresh Automatically

The context manager handles authentication expiration seamlessly during long-running operations:

```python
async def demo_refresh():
    async with await NotebookLMClient.from_storage() as client:
        # If this call encounters an auth error, the core invokes 

        # client.refresh_auth() automatically, updates headers, 

        # and retries the RPC before returning.

        result = await client.chat.ask("nb123", "What is AI?")
        print(result)

```

## Core Architecture and Source Files

The async context manager pattern spans several key files in the `teng-lin/notebooklm-py` repository:

| File | Role |
|------|------|
| [`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py) | Implements `NotebookLMClient` with `__aenter__`, `__aexit__`, `from_storage()` factory, and `refresh_auth()` callback |
| [`src/notebooklm/_core.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py) | Contains `ClientCore` class managing `httpx.AsyncClient` lifecycle via `open()` and `close()` methods |
| [`src/notebooklm/auth.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/auth.py) | Defines `AuthTokens` dataclass storing cookies, CSRF tokens, and session identifiers |
| [`src/notebooklm/_chat.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_chat.py) (and other `_*.py` modules) | Namespace sub-clients receiving the shared `ClientCore` instance for RPC operations |

## Summary

- **Lazy Initialization**: `NotebookLMClient.from_storage()` creates clients without opening network connections, deferring I/O until context entry.
- **Automatic Resource Management**: The `__aenter__` and `__aexit__` methods in [`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py) guarantee that `httpx.AsyncClient` instances open and close correctly.
- **Resilient Authentication**: The context manager integrates token refresh logic, allowing automatic recovery from auth failures without manual intervention.
- **Connection Sharing**: All API namespaces share a single `ClientCore` instance, ensuring efficient connection reuse throughout the async context.

## Frequently Asked Questions

### What happens if an exception occurs inside the async with block?

The `__aexit__` method receives the exception details via `exc_type`, `exc_val`, and `exc_tb` parameters and still executes `await self._core.close()`, ensuring the HTTP client shuts down properly before the exception propagates upward.

### Can I use NotebookLMClient without the async context manager?

While technically possible by manually calling `await client._core.open()` and `await client._core.close()`, this approach bypasses the safety guarantees of the async context manager pattern and requires you to handle cleanup in exception scenarios manually, which is not recommended.

### How does automatic token refresh work within the context manager?

`ClientCore` stores a reference to `NotebookLMClient.refresh_auth()` as a callback. When any RPC call returns an authentication error, `ClientCore` automatically invokes this callback to refresh tokens, updates the HTTP headers, and retries the failed request—all while maintaining the active context and connection.

### Is the HTTP client shared across different API endpoints?

Yes. Sub-clients like `client.notebooks`, `client.sources`, and `client.chat` all receive the same `ClientCore` instance during `NotebookLMClient` initialization, meaning they share a single `httpx.AsyncClient` and benefit from connection pooling and unified token refresh logic.