Quick answer: PixelArray locks the surface. Delete the array (del pa or pa.close()) before calling blit on the surface. Or use surfarray with numpy, which is faster and handles locks cleanly.

You build a procedural texture by writing pixels to a Surface via PixelArray, then blit the surface. The screen shows the texture’s previous state — or nothing. Your writes happened but aren’t visible. The PixelArray is still alive when you tried to blit.

The Lock

PixelArray obtains a write lock on the surface. While locked:

The fix is to release the lock before drawing.

The Fix

import pygame

surf = pygame.Surface((128, 128))

pa = pygame.PixelArray(surf)
for y in range(128):
    for x in range(128):
        pa[x, y] = (x * 2, y * 2, 0)
del pa   # release the lock

screen.blit(surf, (0, 0))

The del pa destroys the PixelArray instance, releasing the lock. Now the blit reads from a non-locked surface and the writes are visible.

Use a Context Manager

with pygame.PixelArray(surf) as pa:
    for y in range(128):
        for x in range(128):
            pa[x, y] = (x * 2, y * 2, 0)
screen.blit(surf, (0, 0))

The with block releases the lock automatically on exit. Clean and harder to leak.

Faster: surfarray with numpy

PixelArray is slow for bulk edits because every assignment goes through Python. numpy-backed surfarray is much faster:

import numpy as np
import pygame
import pygame.surfarray

surf = pygame.Surface((128, 128))
arr = np.zeros((128, 128, 3), dtype=np.uint8)

# vectorized fill
x_idx = np.arange(128)[:, None]
y_idx = np.arange(128)[None, :]
arr[:, :, 0] = (x_idx * 2).astype(np.uint8)
arr[:, :, 1] = (y_idx * 2).astype(np.uint8)

pygame.surfarray.blit_array(surf, arr)
screen.blit(surf, (0, 0))

Roughly 100× faster for a 128×128 surface and locks handled internally by surfarray.

Per-Pixel Convenience: set_at

For a few pixels — under 1000 — surface.set_at((x, y), color) is the simplest path. It manages its own short-lived lock per call. Don’t use for bulk fills; the per-call overhead is too high.

Verifying

Print surf.get_locks() after your edit code. An empty list means the surface is unlocked and ready to blit. A non-empty list shows a lingering PixelArray reference.

“PixelArray writes go through. The lock just prevents you seeing them until released.”

For procedurally generated textures, numpy + surfarray is the only realistic choice — PixelArray is too slow for animation frame rates.