> ## 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