From 6f86bad2cc825ee45696261c035345ff133e6b12 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Tue, 29 Jul 2025 00:24:07 +0300
Subject: [PATCH 1/9] Drop support for Python 3.9
---
.ci/install.sh | 57 ++++------
.github/mergify.yml | 1 -
.github/workflows/test-cygwin.yml | 150 -------------------------
.github/workflows/test-windows.yml | 4 +-
.github/workflows/test.yml | 5 -
README.md | 3 -
Tests/helper.py | 9 +-
Tests/test_features.py | 5 +-
Tests/test_format_hsv.py | 5 +-
Tests/test_image_transform.py | 5 +-
Tests/test_imagechops.py | 6 +-
Tests/test_imagedraw.py | 9 +-
Tests/test_qt_image_qapplication.py | 46 ++++----
Tests/test_qt_image_toqimage.py | 8 +-
Tests/test_shell_injection.py | 8 +-
checks/check_imaging_leaks.py | 3 +-
docs/index.rst | 4 -
docs/installation/newer-versions.csv | 3 +-
docs/installation/platform-support.rst | 8 +-
docs/reference/internal_modules.rst | 5 -
docs/releasenotes/12.0.0.rst | 6 +
pyproject.toml | 9 +-
src/PIL/GifImagePlugin.py | 4 +-
src/PIL/GimpGradientFile.py | 6 +-
src/PIL/ImageDraw.py | 12 +-
src/PIL/ImageFilter.py | 7 +-
src/PIL/ImageMath.py | 8 +-
src/PIL/ImageQt.py | 21 ++--
src/PIL/ImageSequence.py | 6 +-
src/PIL/PcfFontFile.py | 6 +-
src/PIL/PdfParser.py | 17 +--
src/PIL/TiffImagePlugin.py | 10 +-
src/PIL/_imagingft.pyi | 3 +-
src/PIL/_typing.py | 19 +---
src/PIL/_util.py | 7 +-
tox.ini | 4 +-
36 files changed, 169 insertions(+), 320 deletions(-)
delete mode 100644 .github/workflows/test-cygwin.yml
diff --git a/.ci/install.sh b/.ci/install.sh
index acb84f046d1..a5c525b5661 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -13,18 +13,14 @@ aptget_update()
return 1
fi
}
-if [[ $(uname) != CYGWIN* ]]; then
- aptget_update || aptget_update retry || aptget_update retry
-fi
+aptget_update || aptget_update retry || aptget_update retry
set -e
-if [[ $(uname) != CYGWIN* ]]; then
- sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
- ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
- cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
- sway wl-clipboard libopenblas-dev nasm
-fi
+sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
+ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
+ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
+ sway wl-clipboard libopenblas-dev nasm
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
@@ -40,36 +36,27 @@ python3 -m pip install pyroma
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true
-if [[ $(uname) != CYGWIN* ]]; then
- python3 -m pip install numpy
- # PyQt6 doesn't support PyPy3
- if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
- sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
- # TODO Update condition when pyqt6 supports free-threading
- if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
- fi
+python3 -m pip install numpy
- # Pyroma uses non-isolated build and fails with old setuptools
- if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
- # To match pyproject.toml
- python3 -m pip install "setuptools>=77"
- fi
+# PyQt6 doesn't support PyPy3
+if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
+ sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
+ # TODO Update condition when pyqt6 supports free-threading
+ if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
+fi
- # webp
- pushd depends && ./install_webp.sh && popd
+# webp
+pushd depends && ./install_webp.sh && popd
- # libimagequant
- pushd depends && ./install_imagequant.sh && popd
+# libimagequant
+pushd depends && ./install_imagequant.sh && popd
- # raqm
- pushd depends && ./install_raqm.sh && popd
+# raqm
+pushd depends && ./install_raqm.sh && popd
- # libavif
- pushd depends && ./install_libavif.sh && popd
+# libavif
+pushd depends && ./install_libavif.sh && popd
- # extra test images
- pushd depends && ./install_extra_test_images.sh && popd
-else
- cd depends && ./install_extra_test_images.sh && cd ..
-fi
+# extra test images
+pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/mergify.yml b/.github/mergify.yml
index 9bb089615be..14222db1094 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -8,7 +8,6 @@ pull_request_rules:
- status-success=Docker Test Successful
- status-success=Windows Test Successful
- status-success=MinGW
- - status-success=Cygwin Test Successful
actions:
merge:
method: merge
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
deleted file mode 100644
index 581cd63704b..00000000000
--- a/.github/workflows/test-cygwin.yml
+++ /dev/null
@@ -1,150 +0,0 @@
-name: Test Cygwin
-
-on:
- push:
- branches:
- - "**"
- paths-ignore:
- - ".github/workflows/docs.yml"
- - ".github/workflows/wheels*"
- - ".gitmodules"
- - "docs/**"
- - "wheels/**"
- pull_request:
- paths-ignore:
- - ".github/workflows/docs.yml"
- - ".github/workflows/wheels*"
- - ".gitmodules"
- - "docs/**"
- - "wheels/**"
- workflow_dispatch:
-
-permissions:
- contents: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-env:
- COVERAGE_CORE: sysmon
-
-jobs:
- build:
- runs-on: windows-latest
- strategy:
- fail-fast: false
- matrix:
- python-minor-version: [9]
-
- timeout-minutes: 40
-
- name: Python 3.${{ matrix.python-minor-version }}
-
- steps:
- - name: Fix line endings
- run: |
- git config --global core.autocrlf input
-
- - name: Checkout Pillow
- uses: actions/checkout@v4
- with:
- persist-credentials: false
-
- - name: Install Cygwin
- uses: cygwin/cygwin-install-action@v6
- with:
- packages: >
- gcc-g++
- ghostscript
- git
- ImageMagick
- jpeg
- libfreetype-devel
- libimagequant-devel
- libjpeg-devel
- liblapack-devel
- liblcms2-devel
- libopenjp2-devel
- libraqm-devel
- libtiff-devel
- libwebp-devel
- libxcb-devel
- libxcb-xinerama0
- make
- netpbm
- perl
- python3${{ matrix.python-minor-version }}-cython
- python3${{ matrix.python-minor-version }}-devel
- python3${{ matrix.python-minor-version }}-ipython
- python3${{ matrix.python-minor-version }}-numpy
- python3${{ matrix.python-minor-version }}-sip
- python3${{ matrix.python-minor-version }}-tkinter
- wget
- xorg-server-extra
- zlib-devel
-
- - name: Add Lapack to PATH
- uses: egor-tensin/cleanup-path@v4
- with:
- dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
-
- - name: pip cache
- uses: actions/cache@v4
- with:
- path: 'C:\cygwin\home\runneradmin\.cache\pip'
- key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
- restore-keys: |
- ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
-
- - name: Build system information
- run: |
- dash.exe -c "python3 .github/workflows/system-info.py"
-
- - name: Install dependencies
- run: |
- bash.exe .ci/install.sh
-
- - name: Build
- shell: bash.exe -eo pipefail -o igncr "{0}"
- run: |
- .ci/build.sh
-
- - name: Test
- run: |
- bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
-
- - name: Prepare to upload errors
- if: failure()
- run: |
- dash.exe -c "mkdir -p Tests/errors"
-
- - name: Upload errors
- uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: errors
- path: Tests/errors
-
- - name: After success
- run: |
- bash.exe .ci/after_success.sh
- rm C:\cygwin\bin\bash.EXE
-
- - name: Upload coverage
- uses: codecov/codecov-action@v5
- with:
- files: ./coverage.xml
- flags: GHA_Cygwin
- name: Cygwin Python 3.${{ matrix.python-minor-version }}
- token: ${{ secrets.CODECOV_ORG_TOKEN }}
-
- success:
- permissions:
- contents: none
- needs: build
- runs-on: ubuntu-latest
- name: Cygwin Test Successful
- steps:
- - name: Success
- run: echo Cygwin Test Successful
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 766c506e761..c80bb6eb602 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,11 +35,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
+ python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"]
include:
# Test the oldest Python on 32-bit
- - { python-version: "3.9", architecture: "x86" }
+ - { python-version: "3.10", architecture: "x86" }
timeout-minutes: 45
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d18023dbc97..967af2d69ca 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -49,7 +49,6 @@ jobs:
"3.12",
"3.11",
"3.10",
- "3.9",
]
include:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
@@ -57,10 +56,6 @@ jobs:
# Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
- # M1 only available for 3.10+
- - { os: "macos-13", python-version: "3.9" }
- exclude:
- - { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
diff --git a/README.md b/README.md
index 365d356a00c..8585ef6cbd4 100644
--- a/README.md
+++ b/README.md
@@ -36,9 +36,6 @@ As of 2019, Pillow development is
-
diff --git a/Tests/helper.py b/Tests/helper.py
index df99f5f5571..e0dc8a9d4aa 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -10,17 +10,20 @@
import subprocess
import sys
import tempfile
-from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
-from pathlib import Path
-from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageFile, ImageMath, features
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable, Sequence
+ from pathlib import Path
+ from typing import Any
+
logger = logging.getLogger(__name__)
uploader = None
diff --git a/Tests/test_features.py b/Tests/test_features.py
index 520c25b4645..d11a31ce3d2 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -2,7 +2,6 @@
import io
import re
-from typing import Callable
import pytest
@@ -10,6 +9,10 @@
from .helper import skip_unless_feature
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
def test_check() -> None:
# Check the correctness of the convenience function
diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py
index 9cbf18566ca..861eccc1170 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -2,12 +2,15 @@
import colorsys
import itertools
-from typing import Callable
from PIL import Image
from .helper import assert_image_similar, hopper
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
def int_to_float(i: int) -> float:
return i / 255
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 0429eb99d83..7cf52ddbabe 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import math
-from typing import Callable
import pytest
@@ -9,6 +8,10 @@
from .helper import assert_image_equal, assert_image_similar, hopper
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
class TestImageTransform:
def test_sanity(self) -> None:
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index 4309214f5cc..61812ca7dc9 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -1,11 +1,13 @@
from __future__ import annotations
-from typing import Callable
-
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
BLACK = (0, 0, 0)
BROWN = (127, 64, 0)
CYAN = (0, 255, 255)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index e1dcbc52c61..406d965b4e5 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,13 +1,10 @@
from __future__ import annotations
import os.path
-from collections.abc import Sequence
-from typing import Callable
import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
-from PIL._typing import Coords
from .helper import (
assert_image_equal,
@@ -17,6 +14,12 @@
skip_unless_feature,
)
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable, Sequence
+
+ from PIL._typing import Coords
+
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (190, 190, 190)
diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py
index 82a3e074120..b31e2a4ef25 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -1,8 +1,5 @@
from __future__ import annotations
-from pathlib import Path
-from typing import Union
-
import pytest
from PIL import Image, ImageQt
@@ -11,18 +8,8 @@
TYPE_CHECKING = False
if TYPE_CHECKING:
- import PyQt6
- import PySide6
-
- QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
- QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
- QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
- QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
- QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
- QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
- QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
- QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
- QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
+ from pathlib import Path
+
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@@ -32,11 +19,16 @@
from PyQt6.QtGui import QImage, QPainter, QRegion
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
elif ImageQt.qt_version == "side6":
- from PySide6.QtCore import QPoint
- from PySide6.QtGui import QImage, QPainter, QRegion
- from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
-
- class Example(QWidget): # type: ignore[misc]
+ from PySide6.QtCore import QPoint # type: ignore[assignment]
+ from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment]
+ from PySide6.QtWidgets import ( # type: ignore[assignment]
+ QApplication,
+ QHBoxLayout,
+ QLabel,
+ QWidget,
+ )
+
+ class Example(QWidget):
def __init__(self) -> None:
super().__init__()
@@ -47,9 +39,9 @@ def __init__(self) -> None:
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
# hbox
- QHBoxLayout(self) # type: ignore[operator]
+ QHBoxLayout(self)
- lbl = QLabel(self) # type: ignore[operator]
+ lbl = QLabel(self)
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
- app: QApplication | None = QApplication([]) # type: ignore[operator]
+ app: QApplication | None = QApplication([])
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None:
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
- qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
- painter = QPainter(qimage) # type: ignore[operator]
- image_label = QLabel() # type: ignore[operator]
+ qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32"))
+ painter = QPainter(qimage)
+ image_label = QLabel()
image_label.setPixmap(data)
- image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
+ image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)
diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py
index 8cb7ffb9b40..0004b552153 100644
--- a/Tests/test_qt_image_toqimage.py
+++ b/Tests/test_qt_image_toqimage.py
@@ -1,13 +1,15 @@
from __future__ import annotations
-from pathlib import Path
-
import pytest
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from pathlib import Path
+
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
@@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
- assert isinstance(data, QImage) # type: ignore[arg-type, misc]
+ assert isinstance(data, QImage)
assert not data.isNull()
# reload directly from the qimage
diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py
index 38d46f312ed..465517bb699 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -2,8 +2,6 @@
import shutil
from io import BytesIO
-from pathlib import Path
-from typing import IO, Callable
import pytest
@@ -11,6 +9,12 @@
from .helper import djpeg_available, is_win32, netpbm_available
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from pathlib import Path
+ from typing import IO
+
TEST_JPG = "Tests/images/hopper.jpg"
TEST_GIF = "Tests/images/hopper.gif"
diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py
index 231789ca0a0..a1d59ed9c8b 100755
--- a/checks/check_imaging_leaks.py
+++ b/checks/check_imaging_leaks.py
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
from __future__ import annotations
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
import pytest
diff --git a/docs/index.rst b/docs/index.rst
index 689088d48ce..ee51621ac7a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 11,Yes,Yes,Yes,Yes,Yes,,,,
+Pillow 12,Yes,Yes,Yes,Yes,,,,,
+Pillow 11,Yes,Yes,Yes,Yes,Yes,,,,
Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,,
Pillow 10.0,,,Yes,Yes,Yes,Yes,,,
Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,,
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index c2227f1d29f..0026b42892a 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -44,20 +44,18 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
+| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2022 | 3.9 | x86 |
+| Windows Server 2022 | 3.10 | x86 |
| +----------------------------+---------------------+
-| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
+| | 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
-| +----------------------------+---------------------+
-| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 19f78864d1d..41a8837b30a 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -53,11 +53,6 @@ on some Python versions.
An object that supports the read method.
-.. py:data:: TypeGuard
- :value: typing.TypeGuard
-
- See :py:obj:`typing.TypeGuard`.
-
:mod:`~PIL._util` module
------------------------
diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst
index 6c0cd4dba01..5959ac6af1e 100644
--- a/docs/releasenotes/12.0.0.rst
+++ b/docs/releasenotes/12.0.0.rst
@@ -17,6 +17,12 @@ TODO
Backwards incompatible changes
==============================
+Python 3.9
+^^^^^^^^^^
+
+Pillow has dropped support for Python 3.9,
+which reached end-of-life in October 2025.
+
ImageFile.raise_oserror
^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/pyproject.toml b/pyproject.toml
index 4e8623118ba..2e5346016ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,11 +20,10 @@ license-files = [ "LICENSE" ]
authors = [
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
classifiers = [
"Development Status :: 6 - Mature",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -75,9 +74,6 @@ optional-dependencies.tests = [
"trove-classifiers>=2024.10.12",
]
-optional-dependencies.typing = [
- "typing-extensions; python_version<'3.10'",
-]
optional-dependencies.xmp = [
"defusedxml",
]
@@ -190,6 +186,7 @@ lint.ignore = [
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
+ "UP038", # pyupgrade: deprecated rule
]
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
"I002",
@@ -215,7 +212,7 @@ testpaths = [
]
[tool.mypy]
-python_version = "3.9"
+python_version = "3.10"
pretty = true
disallow_any_generics = true
enable_error_code = "ignore-without-code"
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index b03aa7f1505..0ca965d38a5 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -31,7 +31,7 @@
import subprocess
from enum import IntEnum
from functools import cached_property
-from typing import IO, Any, Literal, NamedTuple, Union, cast
+from typing import IO, Any, Literal, NamedTuple, cast
from . import (
Image,
@@ -535,7 +535,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L")
-_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
+_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
def _normalize_palette(
diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py
index ec62f8e4ebc..5f2691882c4 100644
--- a/src/PIL/GimpGradientFile.py
+++ b/src/PIL/GimpGradientFile.py
@@ -21,10 +21,14 @@
from __future__ import annotations
from math import log, pi, sin, sqrt
-from typing import IO, Callable
from ._binary import o8
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import IO
+
EPSILON = 1e-10
"""""" # Enable auto-doc for data member
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index e95fa91f8b3..ae092345ac2 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -33,21 +33,23 @@
import math
import struct
-from collections.abc import Sequence
-from types import ModuleType
-from typing import Any, AnyStr, Callable, Union, cast
+from collections.abc import Callable, Sequence
+from typing import cast
from . import Image, ImageColor
-from ._typing import Coords
# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] = Image.core.outline
TYPE_CHECKING = False
if TYPE_CHECKING:
+ from types import ModuleType
+ from typing import Any, AnyStr
+
from . import ImageDraw2, ImageFont
+ from ._typing import Coords
-_Ink = Union[float, tuple[int, ...], str]
+_Ink = float | tuple[int, ...] | str
"""
A simple 2D drawing interface for PIL images.
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index b9ed54ab20a..9326eeeda9d 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -19,11 +19,14 @@
import abc
import functools
from collections.abc import Sequence
-from types import ModuleType
-from typing import Any, Callable, cast
+from typing import cast
TYPE_CHECKING = False
if TYPE_CHECKING:
+ from collections.abc import Callable
+ from types import ModuleType
+ from typing import Any
+
from . import _imaging
from ._typing import NumpyArray
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index d2504b1ae5a..dfdc50c0552 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -17,11 +17,15 @@
from __future__ import annotations
import builtins
-from types import CodeType
-from typing import Any, Callable
from . import Image, _imagingmath
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from types import CodeType
+ from typing import Any
+
class _Operand:
"""Wraps an image operand, providing standard operators"""
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index df7a57b652c..af4d0742d6b 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -19,23 +19,18 @@
import sys
from io import BytesIO
-from typing import Any, Callable, Union
from . import Image
from ._util import is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
- import PyQt6
- import PySide6
+ from collections.abc import Callable
+ from typing import Any
from . import ImageFile
QBuffer: type
- QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
- QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
- QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
- QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
qt_version: str | None
qt_versions = [
@@ -49,11 +44,15 @@
try:
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
- from PyQt6.QtCore import QBuffer, QIODevice
+ from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba
elif qt_module == "PySide6":
- from PySide6.QtCore import QBuffer, QIODevice
- from PySide6.QtGui import QImage, QPixmap, qRgba
+ from PySide6.QtCore import ( # type: ignore[assignment]
+ QBuffer,
+ QByteArray,
+ QIODevice,
+ )
+ from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment]
except (ImportError, RuntimeError):
continue
qt_is_installed = True
@@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
if qt_is_installed:
- class ImageQt(QImage): # type: ignore[misc]
+ class ImageQt(QImage):
def __init__(self, im: Image.Image | str | QByteArray) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index a6fc340d55f..361be48971e 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -16,10 +16,12 @@
##
from __future__ import annotations
-from typing import Callable
-
from . import Image
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
class Iterator:
"""
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index 0d1968b140a..a00e9b91984 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -18,7 +18,6 @@
from __future__ import annotations
import io
-from typing import BinaryIO, Callable
from . import FontFile, Image
from ._binary import i8
@@ -27,6 +26,11 @@
from ._binary import i32be as b32
from ._binary import i32le as l32
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import BinaryIO
+
# --------------------------------------------------------------------
# declarations
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 73d8c21c023..2c9031469ad 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -8,7 +8,15 @@
import re
import time
import zlib
-from typing import IO, Any, NamedTuple, Union
+from typing import Any, NamedTuple
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import IO
+
+ _DictBase = collections.UserDict[str | bytes, Any]
+else:
+ _DictBase = collections.UserDict
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@@ -251,13 +259,6 @@ def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- _DictBase = collections.UserDict[Union[str, bytes], Any]
-else:
- _DictBase = collections.UserDict
-
-
class PdfDict(_DictBase):
def __setattr__(self, key: str, value: Any) -> None:
if key == "data":
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index c1850f084c0..c1741284b9f 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -47,22 +47,24 @@
import os
import struct
import warnings
-from collections.abc import Iterator, MutableMapping
+from collections.abc import Callable, MutableMapping
from fractions import Fraction
from numbers import Number, Rational
-from typing import IO, Any, Callable, NoReturn, cast
+from typing import IO, Any, cast
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
-from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
from .TiffTags import TYPES
TYPE_CHECKING = False
if TYPE_CHECKING:
- from ._typing import Buffer, IntegralLike
+ from collections.abc import Iterator
+ from typing import NoReturn
+
+ from ._typing import Buffer, IntegralLike, StrOrBytesPath
logger = logging.getLogger(__name__)
diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi
index 1cb1429d6cf..2136810ba6a 100644
--- a/src/PIL/_imagingft.pyi
+++ b/src/PIL/_imagingft.pyi
@@ -1,4 +1,5 @@
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
from . import ImageFont, _imaging
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
index 373938e71e0..e568a469bd8 100644
--- a/src/PIL/_typing.py
+++ b/src/PIL/_typing.py
@@ -3,7 +3,7 @@
import os
import sys
from collections.abc import Sequence
-from typing import Any, Protocol, TypeVar, Union
+from typing import Any, Protocol, TypeVar
TYPE_CHECKING = False
if TYPE_CHECKING:
@@ -26,19 +26,8 @@
else:
Buffer = Any
-if sys.version_info >= (3, 10):
- from typing import TypeGuard
-else:
- try:
- from typing_extensions import TypeGuard
- except ImportError:
-
- class TypeGuard: # type: ignore[no-redef]
- def __class_getitem__(cls, item: Any) -> type[bool]:
- return bool
-
-Coords = Union[Sequence[float], Sequence[Sequence[float]]]
+Coords = Sequence[float] | Sequence[Sequence[float]]
_T_co = TypeVar("_T_co", covariant=True)
@@ -48,7 +37,7 @@ class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ..., /) -> _T_co: ...
-StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]
+StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes]
-__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]
+__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"]
diff --git a/src/PIL/_util.py b/src/PIL/_util.py
index 8ef0d36f754..b1fa6a0f39e 100644
--- a/src/PIL/_util.py
+++ b/src/PIL/_util.py
@@ -1,9 +1,12 @@
from __future__ import annotations
import os
-from typing import Any, NoReturn
-from ._typing import StrOrBytesPath, TypeGuard
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Any, NoReturn, TypeGuard
+
+ from ._typing import StrOrBytesPath
def is_path(f: Any) -> TypeGuard[StrOrBytesPath]:
diff --git a/tox.ini b/tox.ini
index 967d4b53768..8933945b1af 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 314, 313, 312, 311, 310, 39}
+ py{py3, 314, 313, 312, 311, 310}
[testenv]
deps =
@@ -29,7 +29,5 @@ commands =
skip_install = true
deps =
-r .ci/requirements-mypy.txt
-extras =
- typing
commands =
mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs}
From c688d40bf3620c44593563bf6b17b3aad1e1d1ad Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 30 Jul 2025 20:04:03 +1000
Subject: [PATCH 2/9] Python 3.9 is no longer tested on macOS
---
docs/installation/platform-support.rst | 2 --
1 file changed, 2 deletions(-)
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 0026b42892a..f8d6714d04c 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -37,8 +37,6 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| macOS 13 Ventura | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | 3.14, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
From b0542eae1204e4073b4575f49ddfbc2d2afb8339 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 11:15:16 +1000
Subject: [PATCH 3/9] Apply PYI026
---
pyproject.toml | 1 -
src/PIL/_imagingcms.pyi | 8 ++++----
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index a9d7ea03116..137726a1c04 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -185,7 +185,6 @@ lint.ignore = [
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT017", # pytest-assert-in-except
- "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
"UP038", # pyupgrade: deprecated rule
]
diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi
index ddcf93ab1eb..4fc0d60ab79 100644
--- a/src/PIL/_imagingcms.pyi
+++ b/src/PIL/_imagingcms.pyi
@@ -1,14 +1,14 @@
import datetime
import sys
-from typing import Literal, SupportsFloat, TypedDict
+from typing import Literal, SupportsFloat, TypeAlias, TypedDict
from ._typing import CapsuleType
littlecms_version: str | None
-_Tuple3f = tuple[float, float, float]
-_Tuple2x3f = tuple[_Tuple3f, _Tuple3f]
-_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f]
+_Tuple3f: TypeAlias = tuple[float, float, float]
+_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f]
+_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f]
class _IccMeasurementCondition(TypedDict):
observer: int
From ed22b7bc78f4f4bace61a5e825066661f61cbae1 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 11:19:06 +1000
Subject: [PATCH 4/9] Updated match
---
Tests/test_imagecms.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 8b5d88ac883..1953458bcae 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -208,9 +208,10 @@ def test_exceptions() -> None:
ImageCms.getProfileName(None) # type: ignore[arg-type]
skip_missing()
- # Python <= 3.9: "an integer is required (got type NoneType)"
- # Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
- with pytest.raises(ImageCms.PyCMSError, match="integer"):
+ with pytest.raises(
+ ImageCms.PyCMSError,
+ match="'NoneType' object cannot be interpreted as an integer",
+ ):
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
From 002c085d49545130020a35fe7e7208eddb6a1a69 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 11:20:26 +1000
Subject: [PATCH 5/9] Moved install into alphabetical order
---
.ci/install.sh | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.ci/install.sh b/.ci/install.sh
index a5c525b5661..2178c664626 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -27,6 +27,7 @@ python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
+python3 -m pip install numpy
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@@ -36,9 +37,6 @@ python3 -m pip install pyroma
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true
-
-python3 -m pip install numpy
-
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
From 83685110fd8d80ab4475957806f7cb5573291fa1 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 13:43:35 +1000
Subject: [PATCH 6/9] Updated CI targets
---
docs/installation/platform-support.rst | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index f8d6714d04c..c7875914e20 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -19,13 +19,13 @@ These platforms are built and tested for every change.
+==================================+============================+=====================+
| Alpine | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Amazon Linux 2 | 3.9 | x86-64 |
+| Amazon Linux 2 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Amazon Linux 2023 | 3.9 | x86-64 |
+| Amazon Linux 2023 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Arch | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| CentOS Stream 9 | 3.9 | x86-64 |
+| CentOS Stream 9 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
@@ -42,16 +42,16 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, | x86-64 |
-| | 3.12, 3.13, 3.14, PyPy3 | |
+| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
+| | 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.10 | x86 |
| +----------------------------+---------------------+
-| | 3.11, 3.12, 3.13, | x86-64 |
-| | 3.14, PyPy3 | |
+| | 3.11, 3.12, 3.13, 3.14, | x86-64 |
+| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
+----------------------------------+----------------------------+---------------------+
From 60fb0fc9388bea906ee083ad1c7e6198f49d4852 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 12:27:44 +1000
Subject: [PATCH 7/9] Rearranged imports
---
src/PIL/GifImagePlugin.py | 4 +++-
src/PIL/ImageDraw.py | 9 +++++----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 0ca965d38a5..58c460ef3db 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -31,7 +31,7 @@
import subprocess
from enum import IntEnum
from functools import cached_property
-from typing import IO, Any, Literal, NamedTuple, cast
+from typing import Any, NamedTuple, cast
from . import (
Image,
@@ -49,6 +49,8 @@
TYPE_CHECKING = False
if TYPE_CHECKING:
+ from typing import IO, Literal
+
from . import _imaging
from ._typing import Buffer
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index ae092345ac2..ed46899b455 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -33,22 +33,23 @@
import math
import struct
-from collections.abc import Callable, Sequence
+from collections.abc import Sequence
from typing import cast
from . import Image, ImageColor
-# experimental access to the outline API
-Outline: Callable[[], Image.core._Outline] = Image.core.outline
-
TYPE_CHECKING = False
if TYPE_CHECKING:
+ from collections.abc import Callable
from types import ModuleType
from typing import Any, AnyStr
from . import ImageDraw2, ImageFont
from ._typing import Coords
+# experimental access to the outline API
+Outline: Callable[[], Image.core._Outline] = Image.core.outline
+
_Ink = float | tuple[int, ...] | str
"""
From 97beaee58e76c4dcf6991a49e4a3ded3c3913ad7 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Sat, 2 Aug 2025 11:40:03 +0300
Subject: [PATCH 8/9] Test macOS Intel with Python 3.10
---
.github/workflows/test.yml | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 967af2d69ca..c075f04d7cc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -51,11 +51,15 @@ jobs:
"3.10",
]
include:
- - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- - { python-version: "3.10", PYTHONOPTIMIZE: 2 }
+ - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
+ - { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
+ # Intel
+ - { os: "macos-13", python-version: "3.10" }
+ exclude:
+ - { os: "macos-latest", python-version: "3.10" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
From eeda633439ca62f6e2ac5d7e94d9c4cee1e402cc Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 2 Aug 2025 20:12:49 +1000
Subject: [PATCH 9/9] Updated CI targets
---
docs/installation/platform-support.rst | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index c7875914e20..5cf0276d188 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -37,8 +37,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
-| | 3.14, PyPy3 | |
+| macOS 13 Ventura | 3.10 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 |
+| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+