diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst
index 522e70937..8a8569922 100644
--- a/docs/reference/PixelAccess.rst
+++ b/docs/reference/PixelAccess.rst
@@ -65,7 +65,8 @@ Access using negative indexes is also possible.
 
         Modifies the pixel at x,y. The color is given as a single
         numerical value for single band images, and a tuple for
-        multi-band images
+        multi-band images. In addition to this, RGB and RGBA tuples
+        are accepted for P images.
 
         :param xy: The pixel coordinate, given as (x, y).
         :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 6e3a9719d..092cc8040 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1651,7 +1651,8 @@ class Image(object):
         """
         Modifies the pixel at the given position. The color is given as
         a single numerical value for single-band images, and a tuple for
-        multi-band images.
+        multi-band images. In addition to this, RGB and RGBA tuples are
+        accepted for P images.
 
         Note that this method is relatively slow.  For more extensive changes,
         use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@@ -1674,6 +1675,11 @@ class Image(object):
 
         if self.pyaccess:
             return self.pyaccess.putpixel(xy, value)
+
+        if self.mode == "P" and \
+           isinstance(value, (list, tuple)) and len(value) in [3, 4]:
+            # RGB or RGBA value for a P image
+            value = self.palette.getcolor(value)
         return self.im.putpixel(xy, value)
 
     def remap_palette(self, dest_map, source_palette=None):
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 35abfec81..5df1d400c 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -53,6 +53,8 @@ class PyAccess(object):
 
         # Keep pointer to im object to prevent dereferencing.
         self._im = img.im
+        if self._im.mode == "P":
+            self._palette = img.palette
 
         # Debugging is polluting test traces, only useful here
         # when hacking on PyAccess
@@ -80,6 +82,12 @@ class PyAccess(object):
         if y < 0:
             y = self.ysize + y
         (x, y) = self.check_xy((x, y))
+
+        if self._im.mode == "P" and \
+           isinstance(color, (list, tuple)) and len(color) in [3, 4]:
+            # RGB or RGBA value for a P image
+            color = self._palette.getcolor(color)
+
         return self.set_pixel(x, y, color)
 
     def __getitem__(self, xy):

RFC - allow setting of a RGB color on P images
The current implementation of putpixel requires that the color passed in for P mode images be an integer. 

It would be a useful and reasonable to accept an RGB(A) color, look it up in the palette (or allocate an entry if possible) and then pass that integer into the core putpixel method. 

diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index ae9692b39..481ec5935 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -175,6 +175,12 @@ class TestImageGetPixel(AccessTest):
             self.check(mode, 2**15+1)
             self.check(mode, 2**16-1)
 
+    def test_p_putpixel_rgb_rgba(self):
+        for color in [(255, 0, 0), (255, 0, 0, 255)]:
+            im = Image.new("P", (1, 1), 0)
+            im.putpixel((0, 0), color)
+            self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0))
+
 
 @unittest.skipIf(cffi is None, "No cffi")
 class TestCffiPutPixel(TestImagePutPixel):
@@ -294,6 +300,13 @@ class TestCffi(AccessTest):
                 # pixels can contain garbage if image is released
                 self.assertEqual(px[i, 0], 0)
 
+    def test_p_putpixel_rgb_rgba(self):
+        for color in [(255, 0, 0), (255, 0, 0, 255)]:
+            im = Image.new("P", (1, 1), 0)
+            access = PyAccess.new(im, False)
+            access.putpixel((0, 0), color)
+            self.assertEqual(im.convert("RGB").getpixel((0, 0)), (255, 0, 0))
+
 
 class TestEmbeddable(unittest.TestCase):
     @unittest.skipIf(not sys.platform.startswith('win32') or

pip install -e .[dev]
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc libjpeg-dev zlib1g-dev libtiff5-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev libharfbuzz-dev libfribidi-dev libxcb1-dev
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
blessed==1.20.0
build==1.2.2.post1
certifi==2025.1.31
charset-normalizer==3.4.1
check-manifest==0.50
cov-core==1.15.0
coverage==7.8.0
coveralls==4.0.1
docopt==0.6.2
docutils==0.21.2
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
jarn.viewdoc==2.7
Jinja2==3.1.6
MarkupSafe==3.0.2
olefile==0.47
packaging==24.2
-e git+https://github.com/python-pillow/Pillow.git@37d61f1e9b10055ea0f7f157228809dc9fb852a2#egg=Pillow
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pyproject_hooks==1.2.0
pyroma==4.2
pytest==8.3.5
pytest-cov==6.0.0
pytz==2025.2
requests==2.32.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
trove-classifiers==2025.3.19.19
urllib3==2.3.0
wcwidth==0.2.13
zipp==3.21.0

name: Pillow
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - blessed==1.20.0
      - build==1.2.2.post1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - check-manifest==0.50
      - cov-core==1.15.0
      - coverage==7.8.0
      - coveralls==4.0.1
      - docopt==0.6.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jarn-viewdoc==2.7
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - olefile==0.47
      - packaging==24.2
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyproject-hooks==1.2.0
      - pyroma==4.2
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytz==2025.2
      - requests==2.32.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - trove-classifiers==2025.3.19.19
      - urllib3==2.3.0
      - wcwidth==0.2.13
      - zipp==3.21.0
prefix: /opt/conda/envs/Pillow

Tests/test_image_access.py::TestImageGetPixel::test_p_putpixel_rgb_rgba Tests/test_image_access.py::TestImageGetPixel::test_signedness
Tests/test_image_access.py::TestImagePutPixel::test_sanity Tests/test_image_access.py::TestImagePutPixel::test_sanity_negative_index Tests/test_image_access.py::TestImageGetPixel::test_basic
docs/reference/PixelAccess.rst src/PIL/Image.py src/PIL/PyAccess.py
docs/reference/PixelAccess.rst src/PIL/Image.py src/PIL/PyAccess.py