Skip to content

Inspect arg as Python object, instead of using PyErr_Clear() - #9726

Merged
hugovk merged 9 commits into
python-pillow:mainfrom
radarhere:clear
Sep 7, 2026
Merged

hugovk merged 9 commits into
python-pillow:mainfrom
radarhere:clear

Conversation

@radarhere

@radarhere radarhere commented Jun 28, 2026

Copy link
Copy Markdown
Member

In _convert_transparent(), instead of failing a call to PyArg_ParseTuple(), calling PyErr_Clear() afterwards and then trying again,

Pillow/src/_imaging.c

Lines 1084 to 1089 in 095bbc3

if (PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) {
const ModeID mode = findModeID(mode_name);
return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b));
}
PyErr_Clear();
if (PyArg_ParseTuple(args, "si", &mode_name, &r)) {

an alternative strategy would be to parse the second argument as a PyObject ("O") and then check if it is a tuple or not.

A similar change could be made in _convert_matrix().

Comment thread src/_imaging.c Outdated
if (!PyArg_ParseTuple(args, "sO", &mode_name, &matrix)) {
return NULL;
}
if (PyTuple_Size(matrix) == 12) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails with lists:

from PIL import Image

rgb = Image.new("RGB", (8, 8), (255, 0, 0))


def transparent(t):
    rgb.info["transparency"] = t
    return rgb.convert("RGBA")


cases = {
    "matrix tuple(4)": lambda: rgb.convert("L", matrix=(0.3, 0.59, 0.11, 0.0)),
    "matrix list(4)": lambda: rgb.convert("L", matrix=[0.3, 0.59, 0.11, 0.0]),
    "matrix tuple(12)": lambda: rgb.convert("RGB", matrix=tuple(range(12))),
    "matrix list(12)": lambda: rgb.convert("RGB", matrix=list(range(12))),
    "transparency tuple": lambda: transparent((255, 0, 0)),
    "transparency list": lambda: transparent([255, 0, 0]),
}

for label, fn in cases.items():
    try:
        fn()
        print(f"OK    {label}")
    except Exception as e:
        print(f"RAISE {label}: {type(e).__name__}: {e}")

Before:

OK    matrix tuple(4)
OK    matrix list(4)
OK    matrix tuple(12)
OK    matrix list(12)
OK    transparency tuple
OK    transparency list

After:

OK    matrix tuple(4)
RAISE matrix list(4): SystemError: <method 'convert_matrix' of 'ImagingCore' objects> returned a result with an exception set
OK    matrix tuple(12)
RAISE matrix list(12): SystemError: /Users/nad/build_macos_installer/installer/variant/binaries/build_source/Objects/tupleobject.c:96: bad argument to internal function
OK    transparency tuple
RAISE transparency list: TypeError: 'list' object cannot be interpreted as an integer

Maybe worth adding a test?

Suggested change
if (PyTuple_Size(matrix) == 12) {
if (PySequence_Size(matrix) == 12) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matrix should be a tuple?

Pillow/src/PIL/Image.py

Lines 1017 to 1020 in 9c9fece

def convert(
self,
mode: str | None = None,
matrix: tuple[float, ...] | None = None,

Pillow/src/PIL/Image.py

Lines 1054 to 1055 in 9c9fece

:param matrix: An optional conversion matrix. If given, this
should be 4- or 12-tuple containing floating point values.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should according to type hint and docstring, but not enforced.

There is existing code using lists:

https://github.com/search?q=%2F%5C.convert%5C%28.*%2C+matrix%3D%5C%5B%2F&type=code

Do we want to change the behaviour in this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I pushed a commit to allow for lists.

Comment thread src/_imaging.c Outdated
radarhere and others added 2 commits June 30, 2026 13:54
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
@codspeed

codspeed Bot commented Jun 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 603 untouched benchmarks
⏩ 335 skipped benchmarks1


Comparing radarhere:clear (ce207da) with main (ab7e054)

Open in CodSpeed

Footnotes

  1. 335 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment thread src/_imaging.c
Comment on lines +1051 to +1055
PyObject *matrix;
if (!PyArg_ParseTuple(args, "sO", &mode_name, &matrix)) {
return NULL;
}
Py_ssize_t size = PySequence_Size(matrix);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review note to self: it's OK to not decref matrix, since "O" in ParseTuple doesn't create a strong reference.

Comment thread src/_imaging.c Outdated
Comment on lines +1079 to +1082
} else if (!PyArg_ParseTuple(
args, "s(ffff)", &mode_name, m + 0, m + 1, m + 2, m + 3
)) {
return NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, passing in a sequence of any other length than 4 or 12 always yields

argument 2 must be tuple of length 4, not 5

which is a little confusing (since 12 is valid too):

>>> i.convert("L", matrix=(5,5,5,5,5))
Traceback (most recent call last):
  File "<python-input-9>", line 1, in <module>
    i.convert("L", matrix=(5,5,5,5,5))
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "PIL/Image.py", line 1084, in convert
    im = self.im.convert_matrix(mode, matrix)
TypeError: argument 2 must be tuple of length 4, not 5

Could just do

    if (!(size == 4 || size == 12)) {
        return PyErr_Format(PyExc_TypeError, "matrix must be a sequence of 4 or 12 floats");
    }

above instead of the size == -1 check without much extra ado?

@radarhere radarhere Sep 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see, main also behaves that way. Not saying we can't do this idea, just seems to be a bit of scope creep for this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does.

The PR description doesn't really tell the reader why the preimage PyErr_Clear() approach was inadvisable, or why this change needs to be done in the first place. In that light, I assume the intent could be "Improve error handling in convert and convert_transparent", in which case improving errors to be more descriptive altogether (in the spirit of #9784) wouldn't be amiss?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created #9956

Comment thread Tests/test_image_convert.py Outdated
Comment on lines +345 to +350
im = hopper("RGB")
im = hopper()
assert im.mode == "RGB"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't im = hopper("RGB") be clear enough about this Hopper's mode?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've pushed a commit.

Comment thread src/_imaging.c
Comment on lines +1098 to -1092
if (PySequence_Check(transparency)) {
if (!PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) {
return NULL;
}
} else if (!PyArg_ParseTuple(args, "si", &mode_name, &r)) {
return NULL;
}
return NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment as above about confusing error messages stands here (the allowed values are either a single integer, or a 3-sequence of integers), but it's much harder to reach from userland, since you'd either need to call i.im.convert_transparent() by hand, or have explicitly set some image's .info["transparency"] to an invalid value. Might not be worth addressing.

>>> i.im.convert_transparent("RGBA", "hello")
Traceback (most recent call last):
  File "<python-input-22>", line 1, in <module>
    i.im.convert_transparent("RGBA", "hello")
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
TypeError: argument 2 must be 3-item tuple, not str
>>> i.im.convert_transparent("RGBA", 123)
<ImagingCore object at 0x1034fca50>
>>> i.im.convert_transparent("RGBA", [1,2,4])
<ImagingCore object at 0x1034ffeb0>

Comment thread Tests/test_image_convert.py Outdated
@hugovk
hugovk merged commit 5eee920 into python-pillow:main Sep 7, 2026
57 of 58 checks passed
@radarhere
radarhere deleted the clear branch September 7, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants