Skip to content

[Bug]: The Python version of Playwright is not compatible with PEP-749/PEP-649 annotation semantics #42857

Closed
microsoft/playwright-python
#3204

Description

Version

1.63.0

Steps to reproduce

On Python 3.14+ execute the following minimal reproducer using pytest with pytest-playwright installed.

from playwright.sync_api import Page

def test_has_title(page: Page) -> None:
    def callback(msg: Unresolvable) -> None:
        pass

    page.on('console', callback)
    page.goto("https://playwright.dev/")

Expected behavior

The given example passes without errors

Actual behavior

NameError is emitted for Unresolved despite not being needed by playwright in order to function correctly.

Additional context

The Python version makes use of inspect.signature to inspect the parameters of callbacks passed to methods like Page.on, but does not make use of the newly added format parameter in Python 3.14+, despite not needing to inspect the annotations.

It's best practice to use Format.FORWARDREF or Format.STRING if you don't need to inspect annotations, so the deferred execution of the annotations does not result in a NameError for any symbols only defined inside an if TYPE_CHECKING block.

I recommend writing a little compat helper like the following:

import inspect
import sys

if sys.version_info < (3, 14):
    def get_signature(fn):
        return inspect.signature(fn)
else:
    from annotationlib import Format

    def get_signature(fn):
         return inspect.signature(fn, annotation_format=Format.FORWARDREF)

And using that instead of directly calling inspect.signature in the two places playwright currently uses it.

Environment

System:
    OS: Linux 7.2 openSUSE Tumbleweed 20260915
    CPU: (8) x64 AMD Ryzen 7 PRO 3700U w/ Radeon Vega Mobile Gfx
    Memory: 8.13 GB / 21.38 GB
    Container: Yes
  Binaries:
    Node: 24.18.1 - /usr/bin/node
    npm: 2.15.12 - /home/david/git/onegov-cloud/node_modules/.bin/npm
  Languages:
    Bash: 5.3.15 - /usr/bin/bash

Activity

  1. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Hi, I'm the Playwright bot and I took a first look at this.

    Reproduced — this is a real bug on Python 3.14. Both inspect.signature() call sites in playwright-python 1.63.0 evaluate handler annotations under PEP 649/749 deferred semantics, so a handler annotated with a name that only exists under if TYPE_CHECKING: blows up with NameError when the event actually fires:

    from typing import TYPE_CHECKING
    from playwright.sync_api import Page
    
    if TYPE_CHECKING:
        from playwright.sync_api import ConsoleMessage
    
    def test_console_handler(page: Page) -> None:
        def callback(msg: ConsoleMessage) -> None:
            print(msg.text)
    
        page.on("console", callback)
        page.set_content("<script>console.log('hello')</script>")
        page.wait_for_timeout(300)
    FAILED test_real.py::test_console_handler[chromium] - NameError: Page.wait_for_timeout: name 'ConsoleMessage' is not defined
    

    Worth noting this is a 3.14 regression, not a pre-existing wart: the exact same file passes on Python 3.12. And your diagnosis is right on both counts — the annotations are never needed (only parameter.kind is read), and annotation_format=Format.FORWARDREF fixes it. I patched both call sites in an installed 1.63.0 and the repro passes.

    The second call site is worse than the first, because the NameError escapes into add_locator_handler's internals and gets swallowed: instead of a clean failure you get Locator.click: Timeout 30000ms exceeded with locator handler has finished, waiting for locator("#o") to be hidden looping 64 times, and the NameError only surfaces later attached to an unrelated call (Tracing.stop).

    Full matrix — what I ran

    Environment: Linux x64, CPython 3.14.7 and 3.12.3 (via uv), playwright==1.63.0 + pytest-playwright, Chromium headless shell.

    The two call sites, both confirmed present in the installed 1.63.0:

    File Line Caller
    playwright/_impl/_impl_to_api_mapping.py 122 ImplToApiMapping.wrap_handler → wrapper_func
    playwright/_impl/_page.py 128 LocatorHandler.__call__

    Note both evaluate the signature lazily, at dispatch time, not at registration — page.on(...) and page.add_locator_handler(...) return fine; the NameError only lands once the event fires. That's why this can hide behind a timeout.

    Results:

    Case 3.12.3 3.14.7
    bare def cb(msg: Unresolvable), page.on("console", ...) NameError at def time (annotations eager — not a valid repro pre-3.14) NameError at dispatch ✅ repro
    bare def h(loc: Unresolvable), add_locator_handler NameError at def time 30s Locator.click timeout, NameError leaks to Tracing.stop ✅ repro
    if TYPE_CHECKING: import, no __future__ n/a (name undefined at runtime pre-3.14) NameError ✅ repro — the realistic case
    if TYPE_CHECKING: import + from __future__ import annotations passes passes

    That last row is the useful boundary: with from __future__ import annotations the annotations are plain strings and inspect.signature leaves them alone (eval_str=False), so it works on both. It's specifically PEP 649 native deferred annotations — no __future__ import, which is the 3.14 default — that break.

    Isolated check of the mechanism:

    inspect.signature (default)   -> NameError: name 'Unresolvable' is not defined
    signature(FORWARDREF)         -> OK (msg: Unresolvable) -> None
    

    Patching both call sites to annotation_format=Format.FORWARDREF on the installed 1.63.0 clears the NameError on all of the above; the only remaining failure in my scratch suite was my own throwaway locator-handler test, whose handler never hides the overlay, so the click times out for an unrelated reason.

    Not verified: 3.13 with PYTHON_FUTURE_ANNOTATIONS-style opt-in, free-threaded builds, or the async API surface — I only exercised sync_api. The async path goes through the same wrap_handler, so I'd expect it to behave identically.

    One caveat on status: microsoft/playwright-python#3204 is linked here and proposes exactly this fix, but at the time I looked it's still open and unmerged, so nothing has shipped — 1.63.0 is affected and so is current main.

    Triaged on Linux by the Playwright bot - agent run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions