Quick fix

Replace the whole tuple: r, g, b = px[x, y]; px[x, y] = (0, g, b). For whole-image changes, use img.split()/Image.merge() or a NumPy array made with np.array(img) (not np.asarray, which is read-only).

What triggers it

remove_red.py
from PIL import Image

img = Image.open("photo.jpg")
px = img.load()
for x in range(img.width):
    for y in range(img.height):
        px[x, y][0] = 0              # remove the red channel
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

px[x, y] returns a copy of the pixel as a tuple such as (58, 56, 70). Even if Python let you change that tuple, the image would not change, because the tuple is not connected to the image any more.

Fix 1: assign a new tuple

remove_red.py
from PIL import Image

img = Image.open("photo.jpg")
px = img.load()
for x in range(img.width):
    for y in range(img.height):
        r, g, b = px[x, y]
        px[x, y] = (0, g, b)         # assign a complete new tuple
print(img.getpixel((690, 600)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(0, 55, 11)

Correct, but slow: a Python loop over 1.7 million pixels takes around half a second on our test machine. For single pixels, the same idea works with getpixel/putpixel:

one_pixel.py
from PIL import Image

img = Image.open("photo.jpg")
p = img.getpixel((0, 0))
p = (255,) + p[1:]                  # a new tuple: (255, g, b)
img.putpixel((0, 0), p)
print(img.getpixel((0, 0)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(255, 56, 70)

Fix 2: work on whole channels (fast)

Changing a channel everywhere is one operation on that channel:

remove_red_fast.py
from PIL import Image

img = Image.open("photo.jpg")
r, g, b = img.split()                            # one greyscale image per channel
no_red = Image.merge("RGB", (r.point(lambda v: 0), g, b))
print(no_red.getpixel((690, 600)))
no_red.save("no_red.jpg")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(0, 55, 11)
The sunset photo without its red channel: the orange sky turns teal and green
Real output: red channel set to zero.

The NumPy version: assignment destination is read-only

Moving to NumPy is the right instinct, but np.asarray(img) gives a read-only view of Pillow's data:

numpy_red.py
import numpy as np
from PIL import Image

a = np.asarray(Image.open("photo.jpg"))
print("writeable:", a.flags.writeable)
a[:, :, 0] = 0
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
writeable: False
Traceback (most recent call last):
  ...
ValueError: assignment destination is read-only

Use np.array(img), which makes a writable copy (or call .copy() on the read-only array):

numpy_red.py
import numpy as np
from PIL import Image

a = np.array(Image.open("photo.jpg"))           # np.array copies the pixels: writable
print("writeable:", a.flags.writeable)
a[:, :, 0] = 0
print(Image.fromarray(a).getpixel((690, 600)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
writeable: True
(0, 55, 11)

Do not try to "fix" this with a.setflags(write=True): NumPy refuses for arrays it does not own, and the data belongs to Pillow anyway. More in Pillow and NumPy.

Same error, different tuple: img.size

img.size is a tuple too, and it is read-only information about the image, not a setting:

change_width.py
from PIL import Image

img = Image.open("photo.jpg")
img.size[0] = 800
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

To change the size, create a resized image:

change_width.py
from PIL import Image

img = Image.open("photo.jpg")
w, h = img.size
img = img.resize((800, round(h * 800 / w)))      # resize() returns a new image
print(img.size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(800, 534)

The same goes for crop boxes and colours you got from Pillow: turn them into a list (list(box)) if you need to edit them step by step, then pass a tuple back.

The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.