#15438·bokeh

RGB.to_hsl rounds hue to whole degrees, so RGB -> HSL -> RGB is lossy for ~41% of colours

Author: dylanpulverCreated Sep 10, 2026Updated Sep 10, 2026

Software versions

bokeh 3.10.0, Python 3.14.7, macOS. The same code is on main in src/bokeh/colors/color.py.

Browser name and version

N/A, pure Python.

Jupyter notebook / Jupyter Lab version

N/A.

Expected behavior

RGB(...).to_hsl().to_rgb() returns the colour it started from.

Observed behavior

It returns a different colour for about 41% of inputs.

RGB.to_hsl rounds the hue to whole degrees:

python
h, l, s = colorsys.rgb_to_hls(self.r/255., self.g/255., self.b/255.)
return HSL(round(h*360), s, l, self.a)

s and l keep full float precision, and HSL.__init__ annotates h as float, so only the hue is quantised. One degree of hue is worth up to about 2/255 per channel at high saturation.

Sweeping every 5th value per channel, 140,608 colours:

colours that changed worst channel error
as-is 57,858 of 140,608 (41.1%) 2
with h*360 instead of round(h*360) 0 of 140,608 0

Example code

python
from bokeh.colors import RGB
c = RGB(0, 5, 200)
print(c.to_hsl().to_rgb())   # RGB(0, 7, 200, 1.0)

Stack traceback or browser console output

None, no error is raised.


Dropping the rounding would change to_hsl().h from an int to a float, so it is a behaviour change rather than a pure fix. tests/unit/bokeh/colors/test_color__colors.py pins the rounded value (assert c2.h == 24) and would need updating. Raising it as an issue first per the contributing guidelines rather than sending a PR. Happy to send one if you want it changed.

AI assistance was used to find this and to run the sweep above.