From 37ae189714d4486bffa0d7557bfe3fb44251f1d1 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 12 Jun 2026 10:20:08 +0200 Subject: [PATCH 1/2] chore(python): drop Python 3.9 support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/publish.yml | 2 +- .claude/skills/playwright-roll/SKILL.md | 2 +- .github/workflows/ci.yml | 6 +++--- CONTRIBUTING.md | 6 +++--- ROLLING.md | 2 +- playwright/_impl/_browser_type.py | 5 ----- pyproject.toml | 8 ++++---- 7 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index 2d5f9a0a3..fbd916e46 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -38,7 +38,7 @@ extends: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.10' displayName: 'Use Python' - task: NodeTool@0 inputs: diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index c63669f2e..22ab79bd3 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -48,7 +48,7 @@ There is sometimes no `vX.Y.0` tag for the latest release (the bots cut release `CONTRIBUTING.md` covers this. Notes from past rolls: -- The repo says Python 3.9 is required, but 3.9+ works. If `python3.9` isn't available, use `python3` (3.12 is fine). +- The repo requires Python 3.10+. If `python3.10` isn't available, use `python3` (3.12 is fine). - If `python3-venv` is missing system-wide, use `uv venv env` instead, then `uv pip install --python env/bin/python --upgrade pip`. Don't try to `apt install` — sudo is denied in the harness. - Always activate the venv before any `pip`, `pytest`, `mypy`, or `pre-commit` invocation. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8306a565c..7565cd3dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.9', '3.10'] + python-version: ['3.10', '3.14'] browser: [chromium, firefox, webkit] exclude: # WebKit on standard macOS-latest (currently macos-15-arm64) is unstable; @@ -86,10 +86,10 @@ jobs: browser: webkit include: - os: macos-15-xlarge - python-version: '3.9' + python-version: '3.10' browser: webkit - os: macos-15-xlarge - python-version: '3.10' + python-version: '3.14' browser: webkit - os: windows-latest python-version: '3.11' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed7165fe0..d0efe08d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,11 +4,11 @@ ### Configuring python environment -The project development requires Python version 3.9+. To set it as default in the environment run the following commands: +The project development requires Python version 3.10+. To set it as default in the environment run the following commands: ```sh -# You may need to install python 3.9 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.9-venv` -python3.9 -m venv env +# You may need to install python 3.10 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.10-venv` +python3.10 -m venv env source ./env/bin/activate ``` diff --git a/ROLLING.md b/ROLLING.md index 601c6c5b1..8e0623dd8 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -1,7 +1,7 @@ # Rolling Playwright-Python to the latest Playwright driver * checkout repo: `git clone https://github.com/microsoft/playwright-python` -* make sure local python is 3.9 +* make sure local python is 3.10 or newer * create virtual environment, if don't have one: `python -m venv env` * activate venv: `source env/bin/activate` * install all deps: diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 8abac6061..84c212b88 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -15,7 +15,6 @@ import asyncio import json import pathlib -import sys from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast @@ -187,10 +186,6 @@ def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: if not userDataDir: return "" if not Path(userDataDir).is_absolute(): - # Can be dropped once we drop Python 3.9 support (10/2025): - # https://github.com/python/cpython/issues/82852 - if sys.platform == "win32" and sys.version_info[:2] < (3, 10): - return str(pathlib.Path.cwd() / userDataDir) return str(Path(userDataDir).resolve()) return str(Path(userDataDir)) diff --git a/pyproject.toml b/pyproject.toml index 1ba4fff9e..2d385b05b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ readme = "README.md" license = "Apache-2.0" dynamic = ["version"] -requires-python = ">=3.9" +requires-python = ">=3.10" # Please when changing dependencies run the following commands to update requirements.txt: # - pip install uv==0.5.4 # - uv pip compile pyproject.toml -o requirements.txt @@ -24,11 +24,11 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP :: Browsers", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] @@ -70,7 +70,7 @@ asyncio_default_test_loop_scope = "session" [tool.mypy] ignore_missing_imports = true -python_version = "3.9" +python_version = "3.10" warn_unused_ignores = false warn_redundant_casts = true warn_unused_configs = true @@ -88,7 +88,7 @@ profile = "black" [tool.pyright] include = ["playwright", "tests", "scripts"] exclude = ["**/node_modules", "**/__pycache__", "**/.*", "./build"] -pythonVersion = "3.9" +pythonVersion = "3.10" reportMissingImports = false reportTypedDictNotRequiredAccess = false reportCallInDefaultInitializer = true From 1696a296fb1662941705d91ebe8c2fe405ecadad Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 12 Jun 2026 10:41:41 +0200 Subject: [PATCH 2/2] test: avoid string marker false positive Use an object sentinel for the Network.responseReceived leak marker so CPython interned-string dictionaries exposed on Python 3.14 do not look like leaked events. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_reference_count_async.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_reference_count_async.py b/tests/test_reference_count_async.py index 5eb1c28a1..a46f60ef5 100644 --- a/tests/test_reference_count_async.py +++ b/tests/test_reference_count_async.py @@ -24,6 +24,9 @@ from playwright.async_api import async_playwright from tests.server import Server +_NETWORK_RESPONSE_RECEIVED_MARKER = "__pw__is_last_network_response_received_event" +_NETWORK_RESPONSE_RECEIVED_MARKER_VALUE = object() + @pytest.mark.asyncio async def test_memory_objects(server: Server, browser_name: str) -> None: @@ -39,7 +42,9 @@ async def test_memory_objects(server: Server, browser_name: str) -> None: await page.route("**/*", lambda route, _: route.fulfill(body="OK")) def handle_network_response_received(event: Any) -> None: - event["__pw__is_last_network_response_received_event"] = True + event[_NETWORK_RESPONSE_RECEIVED_MARKER] = ( + _NETWORK_RESPONSE_RECEIVED_MARKER_VALUE + ) if browser_name == "chromium": # https://github.com/microsoft/playwright-python/issues/1602 @@ -64,7 +69,13 @@ def handle_network_response_received(event: Any) -> None: assert isinstance(o, dict) name = o.get("_type") # https://github.com/microsoft/playwright-python/issues/1602 - if o.get("__pw__is_last_network_response_received_event", False): + # Python 3.14 can expose CPython's interned-strings dict here, where + # a string can appear as both key and value. Check the sentinel by + # identity to avoid confusing that internal dict with a leaked event. + if ( + o.get(_NETWORK_RESPONSE_RECEIVED_MARKER) + is _NETWORK_RESPONSE_RECEIVED_MARKER_VALUE + ): assert False if not name: continue