Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/compare-dist-sizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

# /// script
# requires-python = ">=3.10"
# requires-python = ">=3.11"
# dependencies = [
# "humanize",
# "prettytable",
Expand Down
2 changes: 1 addition & 1 deletion .github/generate-sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/test-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ concurrency:
cancel-in-progress: true

env:
EXPECTED_DISTS: 87
EXPECTED_DISTS: 78
FORCE_COLOR: 1

jobs:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_bmp_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"),
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_webp_animated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions Tests/test_image_resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image_resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_imageops_usm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
21 changes: 11 additions & 10 deletions docs/installation/newer-versions.csv
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions docs/installation/platform-support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
+----------------------------------+----------------------------+---------------------+
Expand All @@ -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 |
Expand Down
6 changes: 6 additions & 0 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()


Expand Down
2 changes: 1 addition & 1 deletion src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/PcfFontFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 1 addition & 3 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Loading