How the Async Context Manager Pattern Works with NotebookLMClient

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.

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

According to the source code in 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.

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

As implemented in 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 (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.

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 (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.

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:

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:

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 Implements NotebookLMClient with __aenter__, __aexit__, from_storage() factory, and refresh_auth() callback
src/notebooklm/_core.py Contains ClientCore class managing httpx.AsyncClient lifecycle via open() and close() methods
src/notebooklm/auth.py Defines AuthTokens dataclass storing cookies, CSRF tokens, and session identifiers
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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →