Skip to main content
πŸ›‘οΈ Verified Technical Content: Written by Serhii Hrekov. | Last reviewed & updated in Git: July 21, 2026

How to create a 5-color palette where EVERY color is readable against EVERY other color with Python

Β· 6 min read
Serhii Hrekov
Senior Software Engineer & System Architect specializing in Python, Web Systems, Cloud Infrastructure & Automation

Creating a color palette where every color is readable against every other color is a high-level design challenge. As the number of colors in your palette increases, the "contrast space" shrinks significantly.

In this article, we'll build a script that uses an iterative "Collision-Check" algorithm. It generates a candidate color, checks it against every color already in the palette, and only keeps it if it passes the WCAG AA threshold against all of them.


The Logic: How the Generator Works​

To ensure a palette of 5 colors ($C_1, C_2, C_3, C_4, C_5$) is fully accessible, we have to satisfy 10 unique pair combinations.

The algorithm follows these steps:

  1. Seed: Pick a random starting color.
  2. Generate: Create a random candidate color.
  3. Validate: Check the contrast of the candidate against all existing colors in the palette.
  4. Repeat: If it fails any check, discard and try again. If it passes, add it to the set.
  5. Backtrack: If we can't find a match after 500 tries, start the whole palette over (sometimes the first few colors "block" all remaining possibilities).

The Implementation​

I have combined the luminance math from our previous session with the new iterative logic into a single, copy-pasteable script.

# 🎨 Python Script: The "Safe-Set" Accessible Palette Generator

### 1. Requirements
```bash
pip install webcolors

2. The Code​

import random
import webcolors

def get_luminance(rgb_tuple):
"""Calculates relative luminance from an RGB tuple."""
rgb_list = [c / 255 for c in rgb_tuple]
linear_rgb = []
for c in rgb_list:
if c <= 0.03928:
linear_rgb.append(c / 12.92)
else:
linear_rgb.append(((c + 0.055) / 1.055) ** 2.4)
return 0.2126 * linear_rgb[0] + 0.7152 * linear_rgb[1] + 0.0722 * linear_rgb[2]

def get_contrast_ratio(rgb1, rgb2):
l1 = get_luminance(rgb1)
l2 = get_luminance(rgb2)
if l1 < l2:
l1, l2 = l2, l1
return (l1 + 0.05) / (l2 + 0.05)

def generate_random_rgb():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

def generate_safe_palette(size=5, min_ratio=4.5):
"""Generates a palette where EVERY pair meets the min_ratio."""
palette = []
attempts = 0
max_total_attempts = 1000 # Restart palette if stuck

while len(palette) < size:
candidate = generate_random_rgb()

# Check against all existing colors in the palette
is_safe = True
for color in palette:
if get_contrast_ratio(candidate, color) < min_ratio:
is_safe = False
break

if is_safe:
palette.append(candidate)
attempts = 0 # Reset attempts for next color
else:
attempts += 1

# If we can't find a color that fits, the palette is too restricted. Restart.
if attempts > 500:
palette = []
attempts = 0
max_total_attempts -= 1
if max_total_attempts == 0:
return "Error: Could not find a valid palette for this ratio."

return [webcolors.rgb_to_hex(c) for c in palette]

# --- Execution ---
print("πŸš€ Generating a 5-color palette (WCAG AA Compliant)...")
safe_set = generate_safe_palette(size=5, min_ratio=4.5)

if isinstance(safe_set, list):
print(f"\nβœ… Success! Here is your accessible palette:")
for i, hex_code in enumerate(safe_set):
print(f"Color {i+1}: {hex_code.upper()}")
else:
print(safe_set)


πŸ“Š The "Combinatorial Explosion" Problem​

Why is it so hard to get 5 colors to pass? Every new color you add adds $N-1$ new constraints.

Palette SizeContrast Pairs to CheckDifficulty Level
2 Colors1Very Easy
3 Colors3Easy
5 Colors10Hard
10 Colors45Nearly Impossible (at 4.5:1 ratio)

πŸ›‘οΈ Practical Tips for Design​

The "Light/Dark" Anchor Strategy: To make a 5-color palette work, it is usually best to "anchor" the palette with one very dark color (near black) and one very light color (near white). This opens up the "middle ground" for the other three colors to exist without colliding with each other.


πŸ“š Sources & Technical Refs​

  • [1.1] W3C: WCAG 2.1 Contrast Ratio Standard - The source for the 4.5:1 ratio requirement.
  • [2.1] Colormind.io: API Documentation - How deep-learning models are sometimes used to predict "pleasing" but safe palettes.
  • [3.1] Python Docs: Random Module - Efficiency of pseudo-random number generation for large-scale sampling.

More on python