Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use await page.evaluate(...) and return the JavaScript value explicitly. In the usual case, Pyppeteer serializes the returned string, number, boolean, array, or object into a normal Python value:

result = await page.evaluate('''() => ({
    title: document.title,
    href: location.href,
})''')
print(result)

The result above is a Python dictionary. Most surprises—None, an unresolved coroutine, or a value that cannot be transferred—come from the callback syntax, missing await, or returning a browser-only object.

The basic pattern

page.evaluate runs JavaScript in the page and gives the resolved result back to Python. Put the call inside an async function and await it:

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.goto('https://example.com')

    result = await page.evaluate('''() => ({
        title: document.title,
        href: location.href,
        width: document.documentElement.scrollWidth,
        height: document.documentElement.scrollHeight,
    })''')
    print(result)

    await browser.close()

asyncio.get_event_loop().run_until_complete(main())

Use a plain JavaScript return value when you want data in Python. Strings, numbers, booleans, arrays, and ordinary objects are the safest choices.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Block-bodied callbacks need return

An arrow function with braces does not implicitly return a value. This callback returns undefined:

await page.evaluate('''() => {
    document.title;
}''')

Add an explicit return:

title = await page.evaluate('''() => {
    return document.title;
}''')

Parenthesized object syntax is a concise alternative when there is only an expression:

data = await page.evaluate('''() => ({
    title: document.title,
    links: document.links.length,
})''')

Returning an expression string

For a simple expression, pass the expression as a string. If Pyppeteer’s function-versus-expression detection chooses the wrong interpretation, set force_expr=True:

text = await page.evaluate('document.body.textContent', force_expr=True)
print(text)

force_expr=True tells Pyppeteer to evaluate the string as an expression rather than trying to parse it as a function declaration. It is particularly useful for property access, conditional expressions, and other strings that do not look like callback syntax.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Expression or callback: which should you use?

Need Recommended form Example
One property or short expression Expression with force_expr=True document.body.textContent
Several statements Callback with an explicit return () => { const x = ...; return x; }
One object literal Parenthesized object-returning arrow function () => ({ title: document.title })

Passing arguments into evaluate

Arguments come after the JavaScript function string. Pyppeteer supplies them to the callback in the same order:

element = await page.querySelector('h1')
title = await page.evaluate(
    '(element) => element.textContent',
    element,
)
print(title)

This is preferable to interpolating arbitrary Python text into JavaScript. It keeps the callback fixed and lets Pyppeteer marshal the argument.

Pass ordinary values

prefix = 'Price:'
value = 42
result = await page.evaluate(
    '''(prefix, value) => ({
        label: prefix,
        doubled: value * 2,
    })''',
    prefix,
    value,
)
print(result)

For a list or dictionary, pass the Python value as an argument and read it in JavaScript. Keep the data JSON-like so it can be serialized safely.

Pass an element handle

A handle returned by querySelector can be passed as an argument. Read a property from the element inside the page context:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
button = await page.querySelector('button.submit')
if button is None:
    raise RuntimeError('button.submit was not found')

label = await page.evaluate(
    '(node) => node.getAttribute("aria-label") || node.textContent.trim()',
    button,
)
print(label)

Check for None before passing a selector result. A missing element is a selector/state problem, not a value-return problem.

Asynchronous JavaScript callbacks

If the callback returns a Promise, page.evaluate waits for that Promise and returns its resolved value. You can therefore use await inside the browser callback:

payload = await page.evaluate('''async () => {
    const response = await fetch('/data.json');
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
}''')
print(payload)

There are two separate awaits here: Python awaits page.evaluate, while JavaScript awaits fetch and the response body. Omitting either one changes the result or leaves work unfinished.

Wait for a page condition before evaluating

evaluate does not automatically wait for a selector, a framework render, or network activity. Coordinate those events explicitly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.waitForSelector('.results')
count = await page.evaluate('''() => {
    return document.querySelectorAll('.results li').length;
}''')

For a condition that is not represented by a selector, wait in the page and return the final value:

ready = await page.evaluate('''async () => {
    while (!window.appReady) {
        await new Promise(resolve => setTimeout(resolve, 50));
    }
    return window.appReady;
}''')

Use a bounded wait in production so a page that never becomes ready cannot hold the browser indefinitely.

Serializable values versus handles

Normal evaluate returns a serialized value. A DOM node, window, a function, or another browser-owned object is not a useful Python result. Return a projection instead:

summary = await page.evaluate('''() => {
    const article = document.querySelector('article');
    if (!article) return None;
    return {
        text: article.textContent,
        html: article.outerHTML,
        className: article.className,
    };
}''')

If you need to keep working with an in-page object rather than copy its value, use page.evaluateHandle. Pyppeteer returns a JSHandle wrapper for that object:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
handle = await page.evaluateHandle('''() => document.body''')
try:
    body_text = await page.evaluate('(body) => body.innerText', handle)
    print(body_text)
finally:
    await handle.dispose()

A handle remains tied to the browser context and should be disposed when it is no longer needed. Use ordinary evaluate for extracted data; use a handle for continued in-page operations.

Typical return choices

JavaScript result Use
String, number, boolean Directly return it to Python.
Array or plain object Return a structured snapshot of page data.
DOM element Return textContent, outerHTML, attributes, or a plain object.
Long-lived browser object Use evaluateHandle and dispose the resulting handle.
Promise Return it from the callback; Pyppeteer waits for resolution.

Why page.evaluate returns None

The callback returned JavaScript undefined

The most common cause is a block-bodied arrow function without return. Add the statement or use an expression-bodied callback. If the page code intentionally has no result, None is the expected Python representation.

The selector produced no element

querySelector returns null when nothing matches. Return a deliberate value and handle it in Python:

text = await page.evaluate('''() => {
    const node = document.querySelector('.optional');
    return node ? node.textContent : null;
}''')
if text is None:
    print('optional element is absent')

The expression was parsed incorrectly

For a bare expression string, retry with force_expr=True. This removes ambiguity in Pyppeteer’s automatic detection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Python never awaited the coroutine

This is wrong:

result = page.evaluate('() => document.title')

result is a coroutine, not the title. Use:

result = await page.evaluate('() => document.title')

That line must run inside an async function (or another async task that is already being awaited).

The result cannot be serialized

Replace a DOM or browser object with serializable fields. For large structures, select only the properties your Python code needs; this reduces transfer time and avoids circular-reference errors.

Reliable extraction patterns

Return a list of records

rows = await page.evaluate('''() => Array.from(
    document.querySelectorAll('table tbody tr'),
    row => ({
        cells: Array.from(row.cells, cell => cell.textContent.trim()),
        href: row.querySelector('a')?.href || null,
    })
)''')

Normalize values in the browser

Do inexpensive, page-local cleanup before transferring data:

prices = await page.evaluate('''() => Array.from(
    document.querySelectorAll('[data-price]'),
    node => Number(node.dataset.price)
).filter(Number.isFinite)''')

Capture an immutable snapshot

Return a plain object with a timestamp and URL when you need to audit what was read:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
snapshot = await page.evaluate('''() => ({
    capturedAt: new Date().toISOString(),
    url: location.href,
    title: document.title,
    text: document.body.innerText,
})''')

Debugging and failure recovery

Symptom Likely cause Fix
Python receives None No JavaScript return, missing element, or intentional null Add an explicit return and check selector existence.
A coroutine is printed Missing Python await Call result = await page.evaluate(...) in async code.
Expression raises a parsing error Auto-detection treated an expression as a function Pass force_expr=True.
“Object is not serializable” Returned DOM node, function, handle, or circular object Return primitive fields or use evaluateHandle.
Element is null Wrong selector, page not loaded, or content rendered later Navigate first, wait for the selector, then evaluate.
Async fetch never finishes Endpoint, credentials, or page state prevents completion Check the browser console/network path and add an application-level timeout.
Detached execution context Navigation or reload happened while the callback ran Wait for navigation to settle and retry against the new document.

When diagnosing, first reduce the callback to () => 1. If that works, add one operation at a time: selector lookup, property access, transformation, and finally asynchronous work. This isolates browser-state errors from serialization errors.

Performance and safety considerations

  • Return only the fields you need. Sending an entire document or thousands of nodes across the protocol costs more than extracting a compact array.
  • Prefer one callback that computes a related record over many calls that each read one property; each call crosses the browser protocol.
  • Wait for the page state you actually require instead of using a long fixed sleep. A selector or condition is usually both faster and more reliable.
  • Do not inject untrusted strings into JavaScript source. Pass them as arguments, where Pyppeteer handles serialization.
  • Close the browser in a finally block in long-running jobs, and dispose JSHandle objects that are no longer used.
  • Remember that page JavaScript runs with the page’s permissions and origin. It can see page data available to that context, so avoid evaluating secrets or user-controlled code unintentionally.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual goal is a clean image or PDF of a page rather than an in-browser data extraction, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.

For a screenshot, see the ScreenshotNeo documentation and run:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request from Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also offers an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. Every plan includes its features; the Free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently asked questions

Can I return multiple values?

Yes. Return one plain object or array containing the fields. This is usually clearer and more efficient than separate evaluations.

Does evaluate run in Python or in the browser?

The callback runs in the page’s browser context. Python receives the serialized result after the callback completes.

Should I use evaluateHandle for every DOM query?

No. Use ordinary evaluate for extracted text, attributes, and records. Use a handle only when you need a live in-page object for further operations.

Can an evaluated callback use Python variables directly?

No. Pass values as arguments after the function string. JavaScript cannot access Python locals unless you explicitly provide them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What happens when the callback throws?

Pyppeteer propagates the browser-side exception to Python. Catch it at the application boundary, log the selector and URL, and decide whether to retry after the page reaches the expected state.

Frequently Asked Questions

Can I return a DOM element directly from page.evaluate?

Return serializable properties such as textContent, outerHTML, or attributes. Use evaluateHandle when you need an object reference.

Why does force_expr=True matter?

It forces a string to be parsed as a JavaScript expression when Pyppeteer’s automatic function/expression detection would otherwise choose incorrectly.

How do I return a value from an async JavaScript callback?

Return the Promise from the callback; page.evaluate waits for it. Await page.evaluate itself in Python.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.