> ## Documentation Index > Fetch the complete documentation index at: https://docs.stagehand.dev/llms.txt > Use this file to discover all available pages before exploring further. # Migrate Python v2 to v3 > Complete migration guide from Stagehand Python SDK v2 to the new Stainless-based v3 SDK This guide helps you migrate from the legacy Stagehand Python SDK to the new Stainless-based SDK with a **Bring Your Own Browser (BYOB)** architecture. The new Python SDK is a pure API client. You manage the browser yourself using Playwright, Selenium, Puppeteer, or any other browser automation tool. The SDK handles only the AI-powered operations. ## Overview of Changes You bring your own browser driver (Playwright, Selenium, etc.). The SDK is now a pure API client that handles AI-powered operations. All operations require an explicit `session_id`. Start a session, perform operations, and end it when done. Scale browsers easily and control multiple browsers at once by passing the session ID for each browser you want to control. Cleaner initialization with dedicated parameters for API keys and configuration. ### Current Limitations The new SDK does **not yet support**: * Custom Python LLM client classes (e.g., `model_client_options`) * However, we do support custom endpoints like Bedrock or LLM proxies as long as they are OpenAI-API compatible *** ## Step-by-Step Migration ### 1. Update Imports ```python theme={null} import asyncio import logging from stagehand import Stagehand, StagehandConfig, configure_logging # Configure logging configure_logging( level=logging.INFO, remove_logger_name=True, quiet_dependencies=True, ) ``` ```python theme={null} import os from playwright.sync_api import sync_playwright from stagehand import Stagehand # Note: Custom logging configuration is not yet supported. # Use standard Python logging if needed: import logging logging.basicConfig(level=logging.INFO) ``` *** ### 2. Client Initialization ```python theme={null} config = StagehandConfig( env="BROWSERBASE", api_key=os.getenv("BROWSERBASE_API_KEY"), project_id=os.getenv("BROWSERBASE_PROJECT_ID"), headless=False, dom_settle_timeout_ms=3000, model_name="google/gemini-2.0-flash", self_heal=True, wait_for_captcha_solves=True, system_prompt="You are a browser automation assistant...", model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}, verbose=2, ) stagehand = Stagehand(config) await stagehand.init() page = stagehand.page ``` ```python theme={null} SDK_VERSION = "3.0.6" # Create the Stagehand API client client = Stagehand( browserbase_api_key=os.environ.get("BROWSERBASE_API_KEY"), browserbase_project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), model_api_key=os.environ.get("MODEL_API_KEY"), ) # Start a session (returns session metadata) start_response = client.sessions.start( model_name="google/gemini-2.0-flash", x_language="python", x_sdk_version=SDK_VERSION, ) session_id = start_response.data.session_id print(f"Session started: {session_id}") # Connect Playwright to the Browserbase session playwright = sync_playwright().start() browser = playwright.chromium.connect_over_cdp( f"wss://connect.browserbase.com?apiKey={os.environ['BROWSERBASE_API_KEY']}&sessionId={session_id}" ) context = browser.contexts[0] page = context.pages[0] if context.pages else context.new_page() ``` **Key differences:** * Configuration options like `dom_settle_timeout_ms`, `self_heal`, `system_prompt`, and `verbose` are not available in the new SDK * `model_name` is specified when starting a session, not in the config * You must connect Playwright separately to interact with the page *** ### 3. Navigation ```python theme={null} await page.goto("https://google.com/") ``` ```python theme={null} # Recommended for simple navigation page.goto("https://google.com/") ``` ```python theme={null} # Use this if you need Stagehand to track navigation state client.sessions.navigate( id=session_id, url="https://google.com/", frame_id="", # Empty string for main frame x_language="python", x_sdk_version=SDK_VERSION, ) ``` *** ### 4. Direct Page Interactions (Playwright) Any direct page manipulation should use Playwright's native API. ```python theme={null} # Click using Playwright locator (this was already Playwright) await page.get_by_role("link", name="About", exact=True).click() # Keyboard input await page.keyboard.press("Enter") ``` ```python theme={null} # Same Playwright API, but synchronous (or use async Playwright if preferred) page.get_by_role("link", name="About", exact=True).click() # Keyboard input page.keyboard.press("Enter") ``` In the old SDK, `page` was a Stagehand-enhanced Playwright page. In the new SDK, `page` is a standard Playwright page. Direct Playwright methods work the same way. *** ### 5. AI-Powered Actions (`act`) ```python theme={null} await page.act("search for openai") ``` ```python theme={null} act_response = client.sessions.act( id=session_id, input="search for openai", x_language="python", x_sdk_version=SDK_VERSION, ) print(f"Act completed: {act_response.data.result.message}") ``` #### Acting on an Observed Element ```python theme={null} observed = await page.observe("find all articles") if observed: await page.act(observed[0]) ``` ```python theme={null} observe_response = client.sessions.observe( id=session_id, instruction="find all articles", x_language="python", x_sdk_version=SDK_VERSION, ) results = observe_response.data.result if results: element = results[0] act_response = client.sessions.act( id=session_id, input=element, # Pass the observed element directly x_language="python", x_sdk_version=SDK_VERSION, ) ``` *** ### 6. Observing Elements (`observe`) ```python theme={null} observed = await page.observe("find all articles") if len(observed) > 0: element = observed[0] print(f"Found element: {element}") ``` ```python theme={null} observe_response = client.sessions.observe( id=session_id, instruction="find all articles", x_language="python", x_sdk_version=SDK_VERSION, ) results = observe_response.data.result print(f"Found {len(results)} possible actions") if results: element = results[0] print(f"Found element: {element.description}") ``` *** ### 7. Extracting Data (`extract`) ```python theme={null} data = await page.extract("extract the first result from the search") print(data.model_dump_json()) ``` ```python theme={null} extract_response = client.sessions.extract( id=session_id, instruction="extract the first result from the search", schema={ "type": "object", "properties": { "title": { "type": "string", "description": "The title of the first search result" }, "url": { "type": "string", "description": "The URL of the first search result" } }, "required": ["title"] }, x_language="python", x_sdk_version=SDK_VERSION, ) extracted_data = extract_response.data.result print(f"Extracted: {extracted_data}") ``` **Key difference:** The new SDK requires an explicit JSON schema for extraction. This provides better type safety and clearer expectations for the AI model. *** ### 8. Closing the Session ```python theme={null} await stagehand.close() ``` ```python theme={null} # Clean up Playwright resources browser.close() playwright.stop() # End the Stagehand session client.sessions.end( id=session_id, x_language="python", x_sdk_version=SDK_VERSION, ) ``` **Important:** Always clean up both Playwright and the Stagehand session. Use a `try/finally` block to ensure cleanup happens even on errors. *** ### 9. Async vs Sync ```python theme={null} async def main(): stagehand = Stagehand(config) await stagehand.init() await page.goto("https://example.com") await stagehand.close() asyncio.run(main()) ``` ```python theme={null} def main(): client = Stagehand(...) with sync_playwright() as playwright: # ... setup browser connection page.goto("https://example.com") # ... cleanup main() ``` ```python theme={null} import asyncio from playwright.async_api import async_playwright async def main(): client = Stagehand(...) # Client is sync, but that's OK async with async_playwright() as playwright: browser = await playwright.chromium.connect_over_cdp(...) # ... async Playwright operations asyncio.run(main()) ``` *** ## Complete Migration Example ```python theme={null} import asyncio import logging import os from dotenv import load_dotenv from stagehand import Stagehand, StagehandConfig, configure_logging configure_logging(level=logging.INFO, remove_logger_name=True, quiet_dependencies=True) load_dotenv() async def main(): config = StagehandConfig( env="BROWSERBASE", api_key=os.getenv("BROWSERBASE_API_KEY"), project_id=os.getenv("BROWSERBASE_PROJECT_ID"), headless=False, model_name="google/gemini-2.0-flash", model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}, verbose=2, ) stagehand = Stagehand(config) await stagehand.init() page = stagehand.page print(f"Session: {stagehand.session_id}") # Navigate await page.goto("https://google.com/") # Direct Playwright interaction await page.get_by_role("link", name="About", exact=True).click() # AI-powered action await page.goto("https://google.com/") await page.act("search for openai") await page.keyboard.press("Enter") # Observe and act observed = await page.observe("find all articles") if observed: await page.act(observed[0]) # Extract data data = await page.extract("extract the first result") print(data.model_dump_json()) await stagehand.close() if __name__ == "__main__": asyncio.run(main()) ``` ```python theme={null} import os import logging from dotenv import load_dotenv from playwright.sync_api import sync_playwright from stagehand import Stagehand # Standard Python logging (custom Stagehand logging not yet supported) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) load_dotenv() SDK_VERSION = "3.0.6" def main(): # Create Stagehand API client client = Stagehand( browserbase_api_key=os.environ.get("BROWSERBASE_API_KEY"), browserbase_project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), model_api_key=os.environ.get("MODEL_API_KEY"), ) # Start a session start_response = client.sessions.start( model_name="google/gemini-2.0-flash", x_language="python", x_sdk_version=SDK_VERSION, ) session_id = start_response.data.session_id logger.info(f"Session started: {session_id}") logger.info(f"View live: https://www.browserbase.com/sessions/{session_id}") # Connect Playwright to the Browserbase session with sync_playwright() as playwright: browser = playwright.chromium.connect_over_cdp( f"wss://connect.browserbase.com?apiKey={os.environ['BROWSERBASE_API_KEY']}&sessionId={session_id}" ) context = browser.contexts[0] page = context.pages[0] if context.pages else context.new_page() try: # Navigate (using Playwright directly) page.goto("https://google.com/") logger.info("Navigated to Google") # Direct Playwright interaction page.get_by_role("link", name="About", exact=True).click() logger.info("Clicked About link") # Navigate back page.goto("https://google.com/") # AI-powered action (using Stagehand API) act_response = client.sessions.act( id=session_id, input="search for openai", x_language="python", x_sdk_version=SDK_VERSION, ) logger.info(f"Act completed: {act_response.data.result.message}") # Keyboard input (using Playwright) page.keyboard.press("Enter") # Wait for results page.wait_for_timeout(2000) # Observe elements (using Stagehand API) observe_response = client.sessions.observe( id=session_id, instruction="find all articles", x_language="python", x_sdk_version=SDK_VERSION, ) results = observe_response.data.result if results: element = results[0] logger.info(f"Found element: {element.description}") # Act on observed element client.sessions.act( id=session_id, input=element, x_language="python", x_sdk_version=SDK_VERSION, ) else: logger.warning("No elements found") # Extract data (using Stagehand API with schema) extract_response = client.sessions.extract( id=session_id, instruction="extract the first result from the search", schema={ "type": "object", "properties": { "title": {"type": "string", "description": "Result title"}, "url": {"type": "string", "description": "Result URL"}, "snippet": {"type": "string", "description": "Result snippet"}, }, "required": ["title"], }, x_language="python", x_sdk_version=SDK_VERSION, ) logger.info(f"Extracted data: {extract_response.data.result}") finally: # Clean up Playwright browser.close() # End the Stagehand session client.sessions.end( id=session_id, x_language="python", x_sdk_version=SDK_VERSION, ) logger.info("Session ended") if __name__ == "__main__": main() ``` *** ## Quick Reference: Method Mapping | Old SDK | New SDK | | --------------------------------- | -------------------------------------------------------------------------- | | `Stagehand(config)` | `Stagehand(browserbase_api_key=..., ...)` | | `await stagehand.init()` | `client.sessions.start(...)` | | `stagehand.page` | Connect Playwright separately | | `stagehand.session_id` | `start_response.data.session_id` | | `await page.goto(url)` | `page.goto(url)` (Playwright) | | `await page.act(instruction)` | `client.sessions.act(id=session_id, input=instruction, ...)` | | `await page.observe(instruction)` | `client.sessions.observe(id=session_id, instruction=..., ...)` | | `await page.extract(instruction)` | `client.sessions.extract(id=session_id, instruction=..., schema=..., ...)` | | `await stagehand.close()` | `browser.close()` + `client.sessions.end(id=session_id, ...)` | | `configure_logging(...)` | Use standard `logging` module | *** ## Troubleshooting Ensure you're using the correct `session_id` returned from `client.sessions.start()`. Make sure your Browserbase API key has the correct permissions and the session is still active. These are required for all session operations. Use `x_language="python"` and `x_sdk_version="3.0.6"` (or the latest version). The new SDK requires an explicit JSON schema. Make sure your schema matches the expected output structure. *** ## Need Help? Full Stagehand documentation Browserbase documentation Report issues or get help