writing/tutorial/2026/07
TutorialJul 17, 2026·26 min read

Browser-Use in Python: Build AI Agents That Control a Real Web Browser

Learn how to build AI browser agents in Python with Browser-Use, the open-source library with more than 79,000 GitHub stars. This hands-on tutorial covers installation, your first agent, browser configuration, structured output with Pydantic, custom tools, secure credential handling, and production tips.

Classic browser automation breaks the moment a website changes. A renamed CSS class, a redesigned checkout flow, an unexpected cookie banner — and your carefully crafted Playwright selectors collapse. Browser-Use flips the model: instead of scripting every click, you hand an LLM a real browser and describe the goal. The agent reads each page, decides what to click, type, and extract, and adapts when the layout changes.

The project has become the reference open-source framework for AI browser agents, with more than 79,000 GitHub stars and a release pace that has not slowed down in 2026 — the team shipped Browser Use CLI 3.0 in early July. In this tutorial you will build browser agents in Python from scratch: a first research agent, then a structured data extractor backed by Pydantic, then an agent with custom tools and safe credential handling.

If you prefer TypeScript, we covered the JavaScript side of this space in our Stagehand and Browserbase tutorial. This one is all Python.

Prerequisites

Before starting, ensure you have:

  • Python 3.11 or newer installed
  • uv as your package manager (see our uv complete guide) — pip works too
  • An API key for at least one LLM provider (OpenAI, Anthropic, or Google Gemini)
  • Basic familiarity with async/await in Python

No Playwright or Selenium experience is required — that is precisely the point.

What You'll Build

By the end of this tutorial you will have three working agents:

  1. A research agent that searches the web and summarizes what it finds
  2. A structured extractor that scrapes Hacker News into validated Pydantic models
  3. A task agent with custom tools that saves results to disk and handles logins without ever exposing your password to the LLM

Along the way you will learn how the agent loop works, how to configure the browser, and how to keep runs cheap and reliable.

How Browser-Use Works

Browser-Use runs a real Chromium browser and drives it through the Chrome DevTools Protocol. On every step, the library:

  1. Captures the current page as a simplified DOM tree plus an optional screenshot
  2. Sends that state to your LLM together with the task description and history
  3. Receives a decision — click element 12, type into element 7, scroll, navigate, or finish
  4. Executes the action and loops

Because the LLM sees the page as content rather than as brittle selectors, the same agent survives redesigns that would kill a scripted bot. The trade-off is cost and latency: every step is an LLM call, which is why model choice and step limits matter — we will cover both.

Step 1: Project Setup

Create a project and install the library:

mkdir browser-agents && cd browser-agents
uv init
uv add browser-use python-dotenv pydantic

Browser-Use downloads its own Chromium build on first run, so there is no separate browser installation step. If you prefer pip:

pip install browser-use python-dotenv pydantic

Create a .env file with the key for whichever provider you plan to use:

# .env — pick one (or several)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=...

Never commit the .env file. Add it to .gitignore immediately — browser agents often end up with access to logged-in sessions, which makes leaked keys doubly dangerous.

Step 2: Your First Agent

Create first_agent.py:

import asyncio
 
from dotenv import load_dotenv
 
load_dotenv()
 
from browser_use import Agent, ChatOpenAI
 
 
async def main():
    llm = ChatOpenAI(model="gpt-5")
 
    agent = Agent(
        task=(
            "Find the number 1 post on Show HN today. "
            "Give me its title, points, and what the project does."
        ),
        llm=llm,
    )
 
    history = await agent.run()
    print(history.final_result())
 
 
if __name__ == "__main__":
    asyncio.run(main())

Run it:

uv run python first_agent.py

A Chromium window opens, the agent navigates to Hacker News, reads the Show HN page, and prints a summary. Watch the terminal: each step logs the agent's reasoning, the action it chose, and the element it acted on. This log is your primary debugging tool.

Two things worth noting in the code:

  • load_dotenv() runs before the browser_use import. The library reads provider keys from the environment at import time in some configurations, so loading the environment first is the safe habit.
  • history.final_result() returns the agent's closing answer as text. The history object also exposes every step, URL visited, and screenshot taken — useful for audits.

Choosing a model

Browser-Use ships first-party chat wrappers, so you swap providers by changing one line:

from browser_use import ChatOpenAI, ChatAnthropic, ChatGoogle
 
llm = ChatOpenAI(model="gpt-5")                     # OpenAI
llm = ChatAnthropic(model="claude-sonnet-5")        # Anthropic
llm = ChatGoogle(model="gemini-3-flash-preview")    # Google, fast and cheap

A practical pattern: prototype with a fast, inexpensive model like Gemini Flash, then move to a stronger model only if the agent gets lost on complex flows. Since every step is one LLM call, a 30-step task with an expensive model adds up quickly.

Step 3: Configure the Browser

The default browser works, but real projects need control over headless mode, window size, and session reuse. Pass a Browser instance to the agent:

from browser_use import Agent, Browser, ChatOpenAI
 
browser = Browser(
    headless=False,                                # show the window while developing
    window_size={"width": 1280, "height": 900},
)
 
agent = Agent(
    task="Compare the pricing pages of Vercel and Netlify and summarize the differences.",
    browser=browser,
    llm=ChatOpenAI(model="gpt-5"),
)

Set headless=True for servers and CI. Keep it visible during development — watching the agent work is the fastest way to understand why it succeeds or fails.

Skipping the boring part with initial_actions

If you already know where the task starts, do not spend LLM calls on navigation. initial_actions executes deterministic actions before the model takes over:

agent = Agent(
    task="Summarize the theories described on this page.",
    initial_actions=[
        {"navigate": {"url": "https://en.wikipedia.org/wiki/Randomness"}},
    ],
    llm=llm,
)

This is the single easiest optimization: a hardcoded navigate costs nothing, while asking the LLM to "go to Wikipedia and search for X" can burn three or four steps.

Step 4: Structured Output with Pydantic

Free-text answers are fine for humans, but pipelines need data. Pass a Pydantic model as output_model_schema and the agent's final answer is validated against it:

import asyncio
 
from dotenv import load_dotenv
 
load_dotenv()
 
from pydantic import BaseModel
 
from browser_use import Agent, ChatOpenAI
 
 
class Post(BaseModel):
    title: str
    url: str
    points: int
    comments: int
 
 
class TopPosts(BaseModel):
    posts: list[Post]
 
 
async def main():
    agent = Agent(
        task="Go to news.ycombinator.com and extract the top 5 posts.",
        llm=ChatOpenAI(model="gpt-5"),
        output_model_schema=TopPosts,
    )
 
    history = await agent.run()
    result = history.structured_output  # a TopPosts instance
 
    for post in result.posts:
        print(f"{post.points:>4} pts  {post.title}")
 
 
if __name__ == "__main__":
    asyncio.run(main())

history.structured_output gives you a fully validated TopPosts object — no regex, no JSON parsing, no "the model added a trailing comment" cleanup. If the extraction cannot satisfy the schema, you find out immediately instead of downstream.

Keep schemas small and flat. A model with 5 clear fields extracts far more reliably than a deeply nested structure with 30 optional fields. If you need more, run several focused agents rather than one giant extraction.

This approach pairs naturally with a scraping pipeline: point the structured agent at pages discovered by a crawler like the one in our Crawlee TypeScript guide, or compare it with the API-based approach in our Firecrawl tutorial.

Step 5: Custom Tools

The built-in actions cover the browser. Custom tools let the agent reach outside the browser — write files, call your APIs, query a database. Register them with the Tools class:

import asyncio
import json
from pathlib import Path
 
from dotenv import load_dotenv
 
load_dotenv()
 
from browser_use import Agent, ChatOpenAI, Tools
 
tools = Tools()
 
 
@tools.action(description="Save extracted data as JSON to the local results folder.")
def save_results(filename: str, data: str) -> str:
    out = Path("results")
    out.mkdir(exist_ok=True)
    path = out / filename
    path.write_text(data, encoding="utf-8")
    return f"Saved to {path}"
 
 
@tools.action(description="Check whether a product price is below the alert threshold.")
def check_price_alert(price: float, threshold: float) -> str:
    if price <= threshold:
        return f"ALERT: price {price} is at or below threshold {threshold}"
    return f"No alert: price {price} is above threshold {threshold}"
 
 
async def main():
    agent = Agent(
        task=(
            "Look up the current price of the Raspberry Pi 5 8GB on the official "
            "raspberrypi.com site. Use check_price_alert with threshold 80. "
            "Then save the product name, price, and alert status with save_results "
            "as price_watch.json."
        ),
        llm=ChatOpenAI(model="gpt-5"),
        tools=tools,
    )
 
    await agent.run()
    print(json.loads(Path("results/price_watch.json").read_text()))
 
 
if __name__ == "__main__":
    asyncio.run(main())

The decorator's description is what the LLM sees, so write it the way you would write a good docstring for a junior developer: what the tool does, and when to use it. Tools can be sync or async, and type hints on parameters are used to build the schema the model fills in.

This is the same tool-calling mental model used by agent frameworks like the Claude Agent SDK — Browser-Use simply adds a browser to the loop.

Step 6: Logins Without Leaking Credentials

The naive way to automate a login is to paste the password into the task string. Do not do that — the task goes to the LLM provider on every step. Browser-Use solves this with sensitive_data: you pass placeholders, and the real values are substituted locally when typing into the page. The model only ever sees the placeholder names.

agent = Agent(
    task=(
        "Log in to https://example.com/login with x_username and x_password, "
        "then open the account dashboard and report the current plan name."
    ),
    llm=llm,
    sensitive_data={
        "x_username": "anis@example.com",
        "x_password": "s3cr3t-from-env",
    },
)

Load the actual values from environment variables, never literals. For two-factor flows, combine this with a custom tool (for example, a get_2fa_code action that reads from your authenticator's API) and tell the agent explicitly in the task to use that tool instead of trying to extract codes from pages.

Only point agents with credentials at sites you own or are authorized to automate, and respect each site's terms of service and robots policies. An autonomous agent with a logged-in session can do everything you can do — scope it accordingly.

Step 7: Making Runs Reliable and Affordable

A few habits separate demo agents from dependable ones:

Cap the steps. Runaway agents burn money. agent.run(max_steps=25) sets a hard ceiling; for most focused tasks 15 to 30 steps is plenty.

Write tasks like specs, not vibes. Compare:

Bad:  "Find some info about laptop prices"
Good: "Go to example-shop.com, search for 'ultrabook 14 inch',
       and extract name and price of the first 5 results.
       If a cookie banner appears, accept it. Do not open product pages."

Numbered steps, explicit success criteria, and instructions for known obstacles (cookie banners, newsletters popups) dramatically raise success rates.

Log everything. The history object records every action, URL, and screenshot. Persist it for unattended runs — when a nightly job fails, the step log tells you exactly where.

Start headless only after it works headed. Most "the agent is stuck" reports are obvious within ten seconds of watching the window.

Testing Your Implementation

Verify each piece end to end:

  1. uv run python first_agent.py — you should see step-by-step logs and a final text summary of the top Show HN post.
  2. Run the structured extractor — it should print five Hacker News titles with point counts, and history.structured_output should be a TopPosts instance (add print(type(result)) to confirm).
  3. Run the tools agent — a results/price_watch.json file should exist and parse as JSON.

If all three pass, you have the full toolkit: perception, structured extraction, and side effects.

Troubleshooting

The agent loops on the same page. The task is probably ambiguous. Add explicit completion criteria ("finish when you have extracted 5 items") and lower max_steps so failures fail fast.

Provider authentication errors. Confirm the key name matches the wrapper: OPENAI_API_KEY for ChatOpenAI, ANTHROPIC_API_KEY for ChatAnthropic, GOOGLE_API_KEY for ChatGoogle, and that load_dotenv() is called before the agent is created.

Chromium fails to launch on a server. Headless Linux boxes may lack system libraries; install the usual Chromium dependencies for your distro, and always set headless=True in server environments.

Structured output validation fails. Simplify the schema, and state in the task exactly which fields to fill. Optional fields with sensible defaults absorb messy pages better than strict required fields.

Costs are higher than expected. Switch prototyping to a cheaper model, use initial_actions to skip navigation steps, and cap max_steps. Step count is the cost driver — a well-specified task finishing in 8 steps costs a fraction of a vague one wandering for 40.

Next Steps

Conclusion

Browser-Use turns the browser into just another tool an LLM can wield. You installed the library, ran a research agent, extracted validated data with Pydantic schemas, extended the agent with custom Python tools, and handled credentials without leaking them to the model. The core loop — describe the goal, let the agent perceive and act, validate the output — is the same whether you are watching one price or running a fleet of nightly extraction jobs.

The pragmatic path forward: pick one small, annoying, repetitive browser task you do every week, write it as a precise task spec, and let an agent own it. That first automated workflow teaches you more about prompt-as-spec engineering than any amount of reading.