Python’s colorsys module converts between RGB, HLS (hue–lightness–saturation), HSV (hue–saturation–value), and YIQ color spaces. All values are normalized to the range [0.0, 1.0]. import colorsys. rgb_to_hsv: h, s, v = colorsys.rgb_to_hsv(r, g, b) — hue [0,1), saturation [0,1], value [0,1]. hsv_to_rgb: r, g, b = colorsys.hsv_to_rgb(h, s, v). rgb_to_hls: h, l, s = colorsys.rgb_to_hls(r, g, b) — note HLS order, not HSL. hls_to_rgb: r, g, b = colorsys.hls_to_rgb(h, l, s). rgb_to_yiq: y, i, q = colorsys.rgb_to_yiq(r, g, b) — Y=luma, I=in-phase, Q=quadrature; ranges vary. yiq_to_rgb: r, g, b = colorsys.yiq_to_rgb(y, i, q). All inputs/outputs are floats in [0,1] for RGB and HSV/HLS; YIQ ranges: Y [0,1], I [-0.596, 0.596], Q [-0.523, 0.523]. To work with (0–255) integer RGB: divide by 255 before passing, multiply by 255 after. Claude Code generates color palettes, gradient generators, hue-rotation tools, accessible contrast checkers, and image recolouring pipelines.
CLAUDE.md for colorsys
## colorsys Stack
- Stdlib: import colorsys
- RGB→HSV: h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
- HSV→RGB: r, g, b = colorsys.hsv_to_rgb(h, s, v); R=int(r*255)
- RGB→HLS: h, l, s = colorsys.rgb_to_hls(r/255, g/255, b/255)
- HLS→RGB: r, g, b = colorsys.hls_to_rgb(h, l, s)
- Notation: all floats in [0,1]; colorsys uses HLS not HSL order
colorsys Color Pipeline
# app/colorsysutil.py — convert, palette, gradient, contrast, harmonies, recolor
from __future__ import annotations
import colorsys
import math
from dataclasses import dataclass
from typing import NamedTuple
# ─────────────────────────────────────────────────────────────────────────────
# 1. Color type and conversion helpers
# ─────────────────────────────────────────────────────────────────────────────
class RGB(NamedTuple):
r: int # 0–255
g: int
b: int
def to_hex(self) -> str:
return f"#{self.r:02X}{self.g:02X}{self.b:02X}"
@classmethod
def from_hex(cls, hex_color: str) -> "RGB":
h = hex_color.lstrip("#")
return cls(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def to_float(self) -> tuple[float, float, float]:
return self.r / 255, self.g / 255, self.b / 255
@classmethod
def from_float(cls, r: float, g: float, b: float) -> "RGB":
return cls(round(r * 255), round(g * 255), round(b * 255))
def __str__(self) -> str:
return f"RGB({self.r}, {self.g}, {self.b}) {self.to_hex()}"
class HSV(NamedTuple):
h: float # 0.0–1.0 (multiply by 360 for degrees)
s: float # 0.0–1.0
v: float # 0.0–1.0
def to_degrees(self) -> tuple[float, float, float]:
return self.h * 360, self.s, self.v
def __str__(self) -> str:
return f"HSV({self.h*360:.1f}°, {self.s*100:.1f}%, {self.v*100:.1f}%)"
class HLS(NamedTuple):
h: float # 0.0–1.0
l: float # 0.0–1.0 (lightness)
s: float # 0.0–1.0 (saturation)
def __str__(self) -> str:
return f"HLS({self.h*360:.1f}°, {self.l*100:.1f}%, {self.s*100:.1f}%)"
def rgb_to_hsv(color: RGB) -> HSV:
h, s, v = colorsys.rgb_to_hsv(*color.to_float())
return HSV(h, s, v)
def hsv_to_rgb(color: HSV) -> RGB:
return RGB.from_float(*colorsys.hsv_to_rgb(color.h, color.s, color.v))
def rgb_to_hls(color: RGB) -> HLS:
h, l, s = colorsys.rgb_to_hls(*color.to_float())
return HLS(h, l, s)
def hls_to_rgb(color: HLS) -> RGB:
return RGB.from_float(*colorsys.hls_to_rgb(color.h, color.l, color.s))
def hex_to_hsv(hex_color: str) -> HSV:
return rgb_to_hsv(RGB.from_hex(hex_color))
def hsv_to_hex(hsv: HSV) -> str:
return hsv_to_rgb(hsv).to_hex()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Color manipulation
# ─────────────────────────────────────────────────────────────────────────────
def rotate_hue(color: RGB, degrees: float) -> RGB:
"""
Rotate the hue of a color by given degrees (0–360).
Example:
red = RGB(255, 0, 0)
green = rotate_hue(red, 120) # 120° rotation
"""
hsv = rgb_to_hsv(color)
new_h = (hsv.h + degrees / 360) % 1.0
return hsv_to_rgb(HSV(new_h, hsv.s, hsv.v))
def adjust_lightness(color: RGB, delta: float) -> RGB:
"""
Lighten (+) or darken (-) a color by changing HLS lightness.
delta: fractional amount to add to lightness, e.g. 0.2 = 20% lighter.
Example:
darker = adjust_lightness(RGB(100, 200, 100), -0.2)
"""
hls = rgb_to_hls(color)
new_l = max(0.0, min(1.0, hls.l + delta))
return hls_to_rgb(HLS(hls.h, new_l, hls.s))
def adjust_saturation(color: RGB, delta: float) -> RGB:
"""
Increase (+) or decrease (-) saturation.
Example:
muted = adjust_saturation(RGB(200, 50, 50), -0.3)
"""
hls = rgb_to_hls(color)
new_s = max(0.0, min(1.0, hls.s + delta))
return hls_to_rgb(HLS(hls.h, hls.l, new_s))
def grayscale(color: RGB) -> RGB:
"""Convert a color to its grayscale equivalent (perceptual luminance)."""
y, _, _ = colorsys.rgb_to_yiq(*color.to_float())
v = round(y * 255)
return RGB(v, v, v)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Palette and gradient generation
# ─────────────────────────────────────────────────────────────────────────────
def hue_palette(
n: int,
saturation: float = 0.8,
value: float = 0.9,
start_hue: float = 0.0,
) -> list[RGB]:
"""
Generate n evenly-spaced hues at fixed saturation and value.
Example:
palette = hue_palette(6)
for color in palette:
print(color)
"""
return [
hsv_to_rgb(HSV((start_hue + i / n) % 1.0, saturation, value))
for i in range(n)
]
def gradient(
start: RGB,
end: RGB,
steps: int = 10,
space: str = "rgb",
) -> list[RGB]:
"""
Generate a smooth gradient between two colors in RGB or HSV space.
space: "rgb" (linear RGB interpolation) or "hsv" (hue wheel interpolation).
Example:
g = gradient(RGB(255, 0, 0), RGB(0, 0, 255), steps=8, space="hsv")
"""
result: list[RGB] = []
for i in range(steps):
t = i / max(steps - 1, 1)
if space == "hsv":
h1, s1, v1 = rgb_to_hsv(start)
h2, s2, v2 = rgb_to_hsv(end)
# Interpolate hue on the short arc
dh = h2 - h1
if dh > 0.5: dh -= 1.0
if dh < -0.5: dh += 1.0
h = (h1 + t * dh) % 1.0
s = s1 + t * (s2 - s1)
v = v1 + t * (v2 - v1)
result.append(hsv_to_rgb(HSV(h, s, v)))
else:
r = round(start.r + t * (end.r - start.r))
g = round(start.g + t * (end.g - start.g))
b = round(start.b + t * (end.b - start.b))
result.append(RGB(r, g, b))
return result
# ─────────────────────────────────────────────────────────────────────────────
# 4. Color harmony schemes
# ─────────────────────────────────────────────────────────────────────────────
def complementary(color: RGB) -> RGB:
"""Return the complementary color (180° hue rotation)."""
return rotate_hue(color, 180)
def analogous(color: RGB, angle: float = 30) -> tuple[RGB, RGB]:
"""Return two analogous colors at ±angle degrees."""
return rotate_hue(color, -angle), rotate_hue(color, angle)
def triadic(color: RGB) -> tuple[RGB, RGB]:
"""Return two colors forming a triadic harmony (120° apart)."""
return rotate_hue(color, 120), rotate_hue(color, 240)
def split_complementary(color: RGB, angle: float = 150) -> tuple[RGB, RGB]:
"""Return split-complementary colors at ±angle from the complement."""
return rotate_hue(color, angle), rotate_hue(color, 360 - angle)
def tetradic(color: RGB) -> tuple[RGB, RGB, RGB]:
"""Return three colors forming a tetradic (square) harmony."""
return rotate_hue(color, 90), rotate_hue(color, 180), rotate_hue(color, 270)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Contrast and accessibility
# ─────────────────────────────────────────────────────────────────────────────
def relative_luminance(color: RGB) -> float:
"""
Compute WCAG 2.1 relative luminance for a color.
Returns a value in [0.0, 1.0] where 0 = black, 1 = white.
"""
def linearize(c: float) -> float:
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = (linearize(x / 255) for x in (color.r, color.g, color.b))
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast_ratio(a: RGB, b: RGB) -> float:
"""
Compute WCAG 2.1 contrast ratio between two colors.
Returns a value in [1, 21]. WCAG AA requires >= 4.5 for normal text.
Example:
ratio = contrast_ratio(RGB(0, 0, 0), RGB(255, 255, 255))
print(ratio) # 21.0
"""
la = relative_luminance(a)
lb = relative_luminance(b)
light = max(la, lb)
dark = min(la, lb)
return (light + 0.05) / (dark + 0.05)
def wcag_level(ratio: float) -> str:
"""Return WCAG compliance level: 'AAA', 'AA', 'AA Large', or 'Fail'."""
if ratio >= 7.0: return "AAA"
if ratio >= 4.5: return "AA"
if ratio >= 3.0: return "AA Large"
return "Fail"
def best_text_color(background: RGB) -> RGB:
"""
Return black or white whichever has better contrast on background.
Example:
text = best_text_color(RGB(0, 120, 200)) # white on dark blue
"""
lum = relative_luminance(background)
return RGB(0, 0, 0) if lum > 0.179 else RGB(255, 255, 255)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== colorsys demo ===")
# ── conversions ────────────────────────────────────────────────────────────
print("\n--- color space conversions ---")
test_colors = [
RGB(255, 0, 0), # pure red
RGB(0, 200, 100), # teal
RGB(128, 0, 128), # purple
RGB(255, 165, 0), # orange (approximate)
]
for color in test_colors:
hsv = rgb_to_hsv(color)
hls = rgb_to_hls(color)
y, _, _ = colorsys.rgb_to_yiq(*color.to_float())
print(f" {color} → {hsv} {hls} Y={y:.3f}")
# ── round-trip ─────────────────────────────────────────────────────────────
print("\n--- round-trip test ---")
for color in test_colors:
back = hsv_to_rgb(rgb_to_hsv(color))
ok = abs(back.r - color.r) <= 1 and abs(back.g - color.g) <= 1 and abs(back.b - color.b) <= 1
print(f" {color.to_hex()} → HSV → {back.to_hex()} ok={ok}")
# ── hue rotation & adjustments ─────────────────────────────────────────────
print("\n--- hue rotation ---")
red = RGB(200, 50, 50)
for deg in [0, 60, 120, 180, 240, 300]:
rotated = rotate_hue(red, deg)
print(f" {deg:3d}°: {rotated}")
# ── palette ────────────────────────────────────────────────────────────────
print("\n--- hue_palette(8) ---")
palette = hue_palette(8, saturation=0.75, value=0.9)
print(" " + " ".join(c.to_hex() for c in palette))
# ── gradient ───────────────────────────────────────────────────────────────
print("\n--- gradient (red → blue, 6 steps, HSV) ---")
grad = gradient(RGB(220, 30, 30), RGB(30, 30, 220), steps=6, space="hsv")
print(" " + " ".join(c.to_hex() for c in grad))
# ── harmonies ──────────────────────────────────────────────────────────────
print("\n--- color harmonies for coral #FF6B6B ---")
coral = RGB.from_hex("#FF6B6B")
comp = complementary(coral)
tri1, tri2 = triadic(coral)
print(f" original: {coral}")
print(f" complementary: {comp}")
print(f" triadic: {tri1} {tri2}")
# ── contrast / accessibility ───────────────────────────────────────────────
print("\n--- contrast ratios ---")
pairs = [
(RGB(0, 0, 0), RGB(255, 255, 255)), # black on white
(RGB(0, 120, 200), RGB(255, 255, 255)), # blue on white
(RGB(50, 50, 50), RGB(80, 80, 80)), # dark gray on slightly lighter gray
]
for fg, bg in pairs:
ratio = contrast_ratio(fg, bg)
level = wcag_level(ratio)
best = best_text_color(bg)
print(f" {fg.to_hex()} on {bg.to_hex()} ratio={ratio:.2f} {level} best_text={best.to_hex()}")
# ── grayscale ──────────────────────────────────────────────────────────────
print("\n--- grayscale ---")
for c in test_colors:
gray = grayscale(c)
print(f" {c.to_hex()} → {gray.to_hex()}")
print("\n=== done ===")
For the PIL.Image / Pillow alternative — PIL.ImageColor.getrgb(), PIL.ImageEnhance, and ImageFilter operate on entire images and support color modes including RGBA, L (grayscale), LAB, and HSV — use Pillow when you need to apply color transformations to images pixel by pixel, or when you need colorimetric accuracy for image editing; use colorsys when you need to convert programmatically generated colors for UI theming, data visualization palette generation, or accessibility contrast checking without the overhead of importing a full image processing library. For the colour-science / colormath alternative — colour (PyPI) supports over 100 color spaces including CIE XYZ, Lab, LCHab, OKLab, and spectral reflectance; colormath provides delta-E color difference and ICC profile transforms — use these when you need perceptually uniform color spaces (OKLab, CIELab) for distance-based matching, color difference metrics (ΔE2000), or professional color management workflows; use colorsys for lightweight HSV/HLS/YIQ conversions with zero dependencies. The Claude Skills 360 bundle includes colorsys skill sets covering RGB/HSV/HLS typed color classes with to_hex()/from_hex(), full bidirectional conversion helpers, rotate_hue()/adjust_lightness()/adjust_saturation()/grayscale() manipulation tools, hue_palette()/gradient() generators, complementary()/triadic()/analogous() harmony schemes, and relative_luminance()/contrast_ratio()/wcag_level()/best_text_color() accessibility tools. Start with the free tier to try color space conversion patterns and colorsys pipeline code generation.