diff --git a/.github/compare-dist-sizes.py b/.github/compare-dist-sizes.py index ed7b9be0ed6..0b4f71793d2 100644 --- a/.github/compare-dist-sizes.py +++ b/.github/compare-dist-sizes.py @@ -10,7 +10,7 @@ """ # /// script -# requires-python = ">=3.10" +# requires-python = ">=3.11" # dependencies = [ # "humanize", # "prettytable", diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py index e0324d1429e..6fecfc29ae4 100755 --- a/.github/generate-sbom.py +++ b/.github/generate-sbom.py @@ -59,7 +59,7 @@ def upstream_diff_b64( def generate(version: str) -> dict: serial = str(uuid.uuid4()) - now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + now = dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") purl = f"pkg:pypi/pillow@{version}" root = Path(__file__).parent.parent thirdparty = root / "src" / "thirdparty" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c45d7c3a548..c66a1513b74 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,7 +47,6 @@ jobs: fedora-43-amd64, fedora-44-amd64, gentoo, - ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, ubuntu-26.04-resolute-amd64, ] diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 20cba79c3e5..f931354a9fd 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,12 +31,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] + python-version: ["pypy3.11", "3.12", "3.13", "3.14", "3.15"] architecture: ["x64"] os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.10", architecture: "x86", os: "windows-2022" } + - { python-version: "3.11", architecture: "x86", os: "windows-2022" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84bba143a04..da539444d00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,15 +45,14 @@ jobs: "3.13", "3.12", "3.11", - "3.10", ] include: - - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - - { python-version: "3.11", PYTHONOPTIMIZE: 2 } + - { python-version: "3.13", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.12", PYTHONOPTIMIZE: 2 } # Intel - - { os: "macos-26-intel", python-version: "3.10" } + - { os: "macos-26-intel", python-version: "3.11" } exclude: - - { os: "macos-latest", python-version: "3.10" } + - { os: "macos-latest", python-version: "3.11" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c158356df14..a038689eca9 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,7 +37,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 87 + EXPECTED_DISTS: 78 FORCE_COLOR: 1 jobs: @@ -81,7 +81,7 @@ jobs: platform: macos os: macos-26-intel cibw_arch: x86_64 - build: "cp3{10,11}*" + build: "cp311*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ea0853100cc..cab4fbdf16d 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -56,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # noqa: PERF203 + except Exception: if os.path.basename(f) in supported: raise @@ -106,7 +106,7 @@ def get_compare(f: str) -> str: assert_image_similar(im_converted, compare_converted, 5) - except Exception as msg: # noqa: PERF203 + except Exception as msg: # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index f8b29cb2c93..c0fec6d8f32 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -603,7 +603,7 @@ def test_svt_optimizations(self, tmp_path: Path) -> None: @skip_unless_feature("avif") class TestAvifAnimation: @contextmanager - def star_frames(self) -> Generator[list[Image.Image], None, None]: + def star_frames(self) -> Generator[list[Image.Image]]: with Image.open("Tests/images/avif/star.png") as f: yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] @@ -676,7 +676,7 @@ def check(temp_file: Path) -> None: # Test appending using a generator def imGenerator( ims: list[Image.Image], - ) -> Generator[Image.Image, None, None]: + ) -> Generator[Image.Image]: yield from ims temp_file2 = tmp_path / "temp_generator.avif" diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7b504233d56..5293094c43a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1191,7 +1191,7 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 8976aea0924..705dac880e5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -33,7 +33,7 @@ @pytest.fixture -def card() -> Generator[ImageFile.ImageFile, None, None]: +def card() -> Generator[ImageFile.ImageFile]: with Image.open("Tests/images/test-card.png") as im: im.load() try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 8bb4ff8db92..3fb8ae62b84 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -164,7 +164,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e442471d1ca..0400934cff0 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -775,7 +775,7 @@ def test_tiff_save_all(self) -> None: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims mp = BytesIO() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 600448fb9e1..7ae93e76b09 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -95,7 +95,7 @@ def check(temp_file: Path) -> None: # Tests appending using a generator def im_generator( ims: list[Image.Image], - ) -> Generator[Image.Image, None, None]: + ) -> Generator[Image.Image]: yield from ims temp_file2 = tmp_path / "temp_generator.webp" diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f188be81ecb..61b2842cc66 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -372,7 +372,7 @@ def test_dirty_pixels_la(self) -> None: class TestCoreResamplePasses: @contextmanager - def count(self, diff: int) -> Generator[None, None, None]: + def count(self, diff: int) -> Generator[None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff @@ -485,9 +485,7 @@ def test_wrong_arguments(self, resample: Image.Resampling) -> None: def resize_tiled( self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int ) -> Image.Image: - def split_range( - size: int, tiles: int - ) -> Generator[tuple[int, int], None, None]: + def split_range(size: int, tiles: int) -> Generator[tuple[int, int]]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 3e8979a5b11..b2abe4c9810 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -179,7 +179,7 @@ def test_cross_platform(self, tmp_path: Path) -> None: @pytest.fixture -def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: +def gradients_image() -> Generator[ImageFile.ImageFile]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 920012d8639..14620c98e47 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -8,7 +8,7 @@ @pytest.fixture -def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]: +def test_images() -> Generator[dict[str, ImageFile.ImageFile]]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv index e948dd5400e..d22c3853a3c 100644 --- a/docs/installation/newer-versions.csv +++ b/docs/installation/newer-versions.csv @@ -1,10 +1,11 @@ -Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow 12,Yes,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,, -Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes +Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow 13,Yes,Yes,Yes,Yes,,,,,, +Pillow 12,Yes,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,, +Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 90321d054fa..eac96a0081a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -23,7 +23,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Arch | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.10 | x86-64 | +| CentOS Stream 9 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 10 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -38,19 +38,19 @@ These platforms are built and tested for every change. | macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | | | 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| macOS 26 Tahoe | 3.10 | x86-64 | +| macOS 26 Tahoe | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| Ubuntu Linux 24.04 LTS (Noble) | 3.11, 3.12, 3.13, | x86-64 | | | 3.14, 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, | | | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10 | x86 | +| Windows Server 2022 | 3.11 | x86 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | +| Windows Server 2025 | 3.12, 3.13, 3.14, | x86-64 | | | 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.14 (MinGW) | x86-64 | diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index d8721fb12d4..62c21a6f512 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -17,6 +17,12 @@ TODO Backwards incompatible changes ============================== +Python 3.10 +^^^^^^^^^^^ + +Pillow has dropped support for Python 3.10, +which reached end-of-life in October 2026. + Saving I mode images as PNG ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index a2366e717ff..2c273b5dd2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,10 @@ license-files = [ "LICENSE" ] authors = [ { name = "Jeffrey 'Alex' Clark", email = "aclark@aclark.net" }, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 6 - Mature", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -189,7 +188,7 @@ max_supported_python = "3.14" [tool.mypy] follow_imports = "silent" -python_version = "3.10" +python_version = "3.11" disallow_any_generics = true disallow_untyped_defs = true warn_redundant_casts = true diff --git a/setup.py b/setup.py index f015a03ac83..04db878320c 100644 --- a/setup.py +++ b/setup.py @@ -301,7 +301,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: # noqa: PERF203 + except Exception: pass return None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7a43a824f5..37df93587f3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -488,7 +488,7 @@ def init() -> bool: try: logger.debug("Importing %s", plugin) __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: # noqa: PERF203 + except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: @@ -3364,8 +3364,8 @@ class SupportsArrowArrayInterface(Protocol): """ def __arrow_c_array__( - self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 - ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + self, requested_schema: PyCapsule = None # type: ignore[name-defined] # noqa: F821 + ) -> tuple[PyCapsule, PyCapsule]: # type: ignore[name-defined] # noqa: F821 raise NotImplementedError() diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 65edf5659fa..0eb686d07a7 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -951,7 +951,7 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: # noqa: PERF203 + except OSError: pass msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 985444106a1..f12506a6666 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -249,7 +249,7 @@ def _load_encoding(self) -> list[int | None]: ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: # noqa: PERF203 + except UnicodeDecodeError: # character is not supported in selected encoding pass diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 472dfcf5802..a820e7ead35 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -465,9 +465,7 @@ def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") __float__ = _delegate("__float__") - # Python >= 3.11 - if hasattr(Fraction, "__int__"): - __int__ = _delegate("__int__") + __int__ = _delegate("__int__") _LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] diff --git a/tox.ini b/tox.ini index 5089d3817b7..8a4ce05a528 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ requires = env_list = lint mypy - py{py3, 315, 314, 313, 312, 311, 310} + py{py3, 315, 314, 313, 312, 311} [testenv] deps =