Skip to content

Reference

Here you can find the reference for the user facing API of Pylette. This consists of the extract_colors function, which is used to extract a color palette from an image, and the Palette and Color classes, which are used to work with the extracted color palette.

Key Features

  • JSON Export: Export palettes to structured JSON format with metadata
  • Hex Colors: Access hex color codes through the Color.hex property
  • Semantic Fields: Export uses semantic field names (rgb, hsv, hls) instead of generic values
  • Metadata: Rich metadata including extraction parameters, timing, and image info
  • Batch Processing: Process multiple images with parallel execution support

pylette.extract_colors

Extracts a set of 'palette_size' colors from the given image.

Parameters:

Name Type Description Default
image ImageInput

The input image.

required
palette_size int

The number of colors to extract.

5
resize int | bool | None

The sample size. The image is downscaled to (resize, resize) before colors are extracted, which bounds runtime; pass None to sample the image at full resolution instead. Smaller values are faster but coarser; larger values are slower but capture more detail. Defaults to 256. (Passing a bool is deprecated: True maps to 256 and False to None.)

256
mode ExtractionMethod | str

The color quantization algorithm to use.

KM
sort_mode Literal['luminance', 'frequency'] | None

The mode to sort colors.

None
alpha_mask_threshold int | None

Optional integer between 0, 255. Any pixel with alpha less than this threshold will be discarded from calculations.

None

Returns: Palette: A palette of the extracted colors.

Guarantees

The returned palette satisfies these invariants for every extraction method (pinned by the property suite in tests/integration/test_invariants.py):

  • len(palette) <= palette_size. Fewer colors are returned when the image has fewer distinct colors than requested.
  • The color frequencies sum to 1.0.
  • Every channel of every Color.rgb is an int in [0, 255].
  • Extraction is deterministic: the same image and arguments always produce the same palette.
  • Colors are ordered by sort_mode — ascending luminance or, by default, descending frequency — and that ordering is stable.
  • Degenerate inputs (a solid color, a 1x1 image, palette_size greater than the number of distinct colors, a partial alpha mask) are handled without error. The one expected failure is an image with no pixels left to sample (e.g. a fully alpha-masked image), which raises :class:~pylette.NoValidPixelsError.

Raises:

Type Description
InvalidImageError

If the image cannot be loaded or its type is unsupported.

NoValidPixelsError

If no pixels remain after alpha masking.

UnknownExtractionMethodError

If mode is not a known extraction method.

Examples:

Colors can be extracted from a variety of sources, including local files, byte streams, URLs, and numpy arrays.

>>> extract_colors("path/to/image.jpg", palette_size=5, resize=256, mode="KM", sort_mode="luminance")
>>> extract_colors(b"image_bytes", palette_size=5, resize=None, mode="KM", sort_mode="luminance")
Source code in pylette/src/color_extraction.py
def extract_colors(
    image: ImageInput,
    palette_size: int = 5,
    resize: int | bool | None = 256,
    mode: ExtractionMethod | str = ExtractionMethod.KM,
    sort_mode: Literal["luminance", "frequency"] | None = None,
    alpha_mask_threshold: int | None = None,
) -> Palette:
    """
    Extracts a set of 'palette_size' colors from the given image.

    Parameters:
        image: The input image.
        palette_size: The number of colors to extract.
        resize: The sample size. The image is downscaled to ``(resize, resize)``
            before colors are extracted, which bounds runtime; pass ``None`` to
            sample the image at full resolution instead. Smaller values are
            faster but coarser; larger values are slower but capture more detail.
            Defaults to ``256``. (Passing a ``bool`` is deprecated: ``True`` maps
            to ``256`` and ``False`` to ``None``.)
        mode: The color quantization algorithm to use.
        sort_mode: The mode to sort colors.
        alpha_mask_threshold: Optional integer between 0, 255.
            Any pixel with alpha less than this threshold will be discarded from calculations.
    Returns:
        Palette: A palette of the extracted colors.

    Guarantees:
        The returned palette satisfies these invariants for every extraction
        method (pinned by the property suite in ``tests/integration/test_invariants.py``):

        - ``len(palette) <= palette_size``. Fewer colors are returned when the
          image has fewer distinct colors than requested.
        - The color frequencies sum to ``1.0``.
        - Every channel of every ``Color.rgb`` is an ``int`` in ``[0, 255]``.
        - Extraction is deterministic: the same image and arguments always
          produce the same palette.
        - Colors are ordered by ``sort_mode`` — ascending ``luminance`` or, by
          default, descending ``frequency`` — and that ordering is stable.
        - Degenerate inputs (a solid color, a 1x1 image, ``palette_size``
          greater than the number of distinct colors, a partial alpha mask) are
          handled without error. The one expected failure is an image with no
          pixels left to sample (e.g. a fully alpha-masked image), which raises
          :class:`~pylette.NoValidPixelsError`.

    Raises:
        InvalidImageError: If the image cannot be loaded or its type is unsupported.
        NoValidPixelsError: If no pixels remain after alpha masking.
        UnknownExtractionMethodError: If ``mode`` is not a known extraction method.

    Examples:
        Colors can be extracted from a variety of sources, including local files, byte streams, URLs, and numpy arrays.

        >>> extract_colors("path/to/image.jpg", palette_size=5, resize=256, mode="KM", sort_mode="luminance")
        >>> extract_colors(b"image_bytes", palette_size=5, resize=None, mode="KM", sort_mode="luminance")
    """

    start_time = time.time()

    mode = coerce_to_enum(mode, ExtractionMethod, error_cls=UnknownExtractionMethodError)
    resize = _resolve_resize(resize)

    source_type = _get_source_type_from_image_input(image)
    # Normalize input to PIL Image and convert to RGBA
    img_obj = _normalize_image_input(image)
    original_size = img_obj.size
    img = img_obj.convert("RGBA")

    # Store original image info
    image_info = ImageInfo(
        original_size=original_size,
        processed_size=(resize, resize) if resize is not None else img.size,
        format=getattr(img_obj, "format", None),
        mode=img.mode,
        has_alpha=img.mode in ("RGBA", "LA") or "transparency" in img_obj.info,
    )

    if resize is not None:
        img = img.resize((resize, resize))
        image_info["processed_size"] = (resize, resize)

    width, height = img.size
    arr = np.asarray(img)

    if alpha_mask_threshold is None:
        alpha_mask_threshold = 0

    alpha_mask = arr[:, :, 3] <= alpha_mask_threshold
    valid_pixels = arr[~alpha_mask]

    if len(valid_pixels) == 0:
        raise NoValidPixelsError(
            f"No valid pixels remain after applying alpha mask with threshold {alpha_mask_threshold}. "
            f"Try using a lower alpha-mask-threshold value or check if your image has transparency."
        )

    # Color extraction
    extractor = get_extractor(mode)
    colors = extractor.extract(arr=valid_pixels, palette_size=palette_size)

    if colors:
        if sort_mode == "luminance":
            colors.sort(key=lambda c: c.luminance, reverse=False)
        else:
            colors.sort(reverse=True)

    end_time = time.time()

    # Build comprehensive metadata
    metadata = PaletteMetaData(
        image_source=_get_descriptive_image_source(image, img_obj),
        source_type=source_type,
        extraction_params=ExtractionParams(
            palette_size=palette_size,
            mode=mode,
            sort_mode=sort_mode,
            resize=resize,
            alpha_mask_threshold=alpha_mask_threshold,
        ),
        image_info=image_info,
        processing_stats=ProcessingStats(
            total_pixels=width * height,
            valid_pixels=len(valid_pixels),
            extraction_time=end_time - start_time,
            timestamp=datetime.now().isoformat(),
        ),
    )

    return Palette(colors, metadata=metadata)

pylette.batch_extract_colors

Extract colors from multiple images in parallel.

Parameters:

Name Type Description Default
progress_callback Callable[[int, BatchResult], None] | None

Optional callback function called when each task completes. Receives (task_number, result) as arguments.

None
Source code in pylette/src/color_extraction.py
def batch_extract_colors(
    images: Sequence[ImageInput],
    palette_size: int = 5,
    resize: int | bool | None = 256,
    mode: ExtractionMethod | str = ExtractionMethod.KM,
    sort_mode: Literal["luminance", "frequency"] | None = None,
    alpha_mask_threshold: int | None = None,
    max_workers: int | None = None,
    progress_callback: Callable[[int, BatchResult], None] | None = None,
) -> list[BatchResult]:
    """Extract colors from multiple images in parallel.

    Args:
        progress_callback: Optional callback function called when each task completes.
                         Receives (task_number, result) as arguments.
    """

    resize = _resolve_resize(resize)

    def thread_fn(image: ImageInput):
        return extract_colors(
            image=image,
            palette_size=palette_size,
            resize=resize,
            mode=mode,
            sort_mode=sort_mode,
            alpha_mask_threshold=alpha_mask_threshold,
        )

    results: list[BatchResult] = []
    task_number = 1

    with ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="pylette") as executor:
        futures_to_image_map = {executor.submit(thread_fn, image): image for image in images}

        for future in as_completed(futures_to_image_map):
            source_image = futures_to_image_map[future]
            try:
                r = future.result()
                batch_result = BatchResult(source=source_image, result=r)
                results.append(batch_result)
                if progress_callback:
                    progress_callback(task_number, batch_result)
            except Exception as e:
                batch_result = BatchResult(source=source_image, exception=e)
                results.append(batch_result)
                if progress_callback:
                    progress_callback(task_number, batch_result)
            task_number += 1

    # Return results in original order
    source_to_result = {r.source: r for r in results}
    return [source_to_result[source] for source in images]

pylette.Palette

Source code in pylette/src/palette.py
class Palette:
    def __init__(self, colors: list[Color], metadata: PaletteMetaData | None = None):
        """
        Initializes a color palette with a list of Color objects.

        Parameters:
            colors (list[Color]): A list of Color objects.

        Note:
            For a palette produced by :func:`~pylette.extract_colors`,
            ``frequencies`` are the per-color relative weights and sum to ``1.0``.
        """

        self.colors = colors
        self.frequencies = [c.frequency for c in colors]
        self.number_of_colors = len(colors)
        self.metadata = metadata

    def _generate_palette_image(self, w: int = 50, h: int = 50) -> Image.Image:
        """
        Helper method to generate a palette image.

        Parameters:
            w (int): Width of each color component.
            h (int): Height of each color component.

        Returns:
            PIL.Image.Image: The generated palette image.
        """
        img = Image.new("RGB", size=(w * self.number_of_colors, h))
        arr = np.asarray(img).copy()
        for i in range(self.number_of_colors):
            c = self.colors[i]
            arr[:, i * h : (i + 1) * h, :] = c.rgb
        return Image.fromarray(arr, "RGB")

    def save(
        self,
        w: int = 50,
        h: int = 50,
        filename: str = "color_palette",
        extension: str = "jpg",
    ) -> None:
        """
        Saves the color palette as an image.

        Parameters:
            w (int): Width of each color component.
            h (int): Height of each color component.
            filename (str): Filename.
            extension (str): File extension.
        """
        img = self._generate_palette_image(w, h)
        img.save(f"{filename}.{extension}")

    def display(
        self,
        w: int = 50,
        h: int = 50,
        save_to_file: bool = False,
        filename: str = "color_palette",
        extension: str = "jpg",
    ) -> None:
        """
        Displays the color palette as an image, with an option for saving the image.

        Parameters:
            w (int): Width of each color component.
            h (int): Height of each color component.
            save_to_file (bool): Whether to save the file or not.
            filename (str): Filename.
            extension (str): File extension.
        """
        img = self._generate_palette_image(w, h)
        img.show()

        if save_to_file:
            self.save(w, h, filename, extension)

    def __getitem__(self, item: int) -> Color:
        return self.colors[item]

    def __len__(self) -> int:
        return self.number_of_colors

    def random_color(self, N: int, mode: str = "frequency") -> list[Color]:
        """
        Returns N random colors from the palette, either using the frequency of each color, or choosing uniformly.

        Parameters:
            N (int): Number of random colors to return.
            mode (str): Mode to use for selection. Can be "frequency" or "uniform".

        Returns:
            list[Color]: List of N random colors from the palette.
        """

        if mode == "frequency":
            # Convert to numpy-compatible format for weighted selection
            colors_array = np.array(range(len(self.colors)))
            indices = np.random.choice(colors_array, size=N, p=self.frequencies)
            return [self.colors[i] for i in indices]
        elif mode == "uniform":
            # Uniform selection without weights
            colors_array = np.array(range(len(self.colors)))
            indices = np.random.choice(colors_array, size=N)
            return [self.colors[i] for i in indices]
        else:
            raise ValueError(f"Invalid mode: {mode}. Must be 'frequency' or 'uniform'.")

    def dedup(self) -> "Palette":
        """Return a new palette with exactly-equal colors merged (frequencies summed).

        Only colorspace-identical colors (same 8-bit RGB) are collapsed; for
        perceptual near-duplicates use :meth:`merge_similar`. The original palette
        is left unchanged.

        Returns:
            Palette: A new palette with exact duplicates removed.
        """
        return Palette(operations.dedup(self.colors))

    def merge_similar(self, delta_e: float) -> "Palette":
        """Return a new palette with perceptually similar colors merged.

        Colors within ``delta_e`` of each other (OKLab ΔE, see
        :meth:`Color.delta_e`) collapse to their frequency-weighted OKLab mean, with
        frequencies summed (so the total stays ≈ 1.0). The original palette is left
        unchanged. For exact duplicates only, use :meth:`dedup`.

        Parameters:
            delta_e (float): Non-negative ΔE threshold; larger merges more.

        Returns:
            Palette: A new palette with near-duplicates merged.
        """
        return Palette(operations.merge_similar(self.colors, delta_e))

    def gradient(self, steps_between: int) -> "Palette":
        """Return a new palette with interpolated colors bridging consecutive swatches.

        Between each consecutive pair of colors, ``steps_between`` OKLab-interpolated
        colors are inserted, producing a smooth ramp across the whole palette. The
        result has equal frequencies summing to 1.0. A palette with fewer than two
        colors has nothing to bridge and is returned unchanged. The original palette
        is left unchanged.

        Parameters:
            steps_between (int): Colors to insert between each pair (>= 1).

        Returns:
            Palette: A new palette holding the gradient.

        Raises:
            ValueError: If ``steps_between`` is less than 1.
        """
        if steps_between < 1:
            raise ValueError(f"steps_between must be at least 1, got {steps_between}.")
        colors = self.colors
        if len(colors) < 2:
            return Palette([Color.from_srgb_float(c.rgb_float, c.frequency, alpha=c.opacity) for c in colors])
        out: list[Color] = []
        for idx in range(len(colors) - 1):
            segment = operations.interpolate(colors[idx], colors[idx + 1], steps_between + 2)
            if idx > 0:
                segment = segment[1:]  # drop the seam color shared with the previous segment
            out.extend(segment)
        return Palette(operations.normalize_frequencies(out))

    def sort_perceptual(self, descending: bool = False) -> "Palette":
        """Return a new palette sorted by perceptual lightness (OKLab L).

        Ascending by default (darkest first). The sort is stable and idempotent.
        The original palette is left unchanged.

        Parameters:
            descending (bool): If True, sort lightest first.

        Returns:
            Palette: A new, perceptually sorted palette.
        """
        return Palette(operations.sort_perceptual(self.colors, descending=descending))

    def harmony(self, kind: HarmonyKind | str) -> "Palette":
        """Return a color-harmony scheme seeded from this palette's dominant color.

        Seeds from the dominant (highest-frequency) color and delegates to the
        harmony operation. An empty palette yields an empty palette.

        Parameters:
            kind (HarmonyKind | str): ``"complementary"``, ``"triadic"``, or
                ``"analogous"``.

        Returns:
            Palette: A new palette holding the harmony scheme.

        Raises:
            InvalidHarmonyError: If ``kind`` is not recognized.
        """
        if not self.colors:
            return Palette([])
        seed = max(self.colors, key=lambda c: c.frequency)
        return Palette(operations.harmony(seed, kind))

    def to_json(
        self,
        filename: str | None = None,
        colorspace: ColorSpace | str = ColorSpace.RGB,
        include_metadata: bool = True,
    ) -> dict[str, object] | None:
        """
        Exports the palette to JSON format.

        Parameters:
            filename (str | None): File to save to. If None, returns the dictionary.
            colorspace (ColorSpace | str): Color space to use (enum member, its
                value, or case-insensitive name).
            include_metadata (bool): Whether to include palette metadata.

        Returns:
            dict | None: The palette data as a dictionary if filename is None.
        """

        colorspace = coerce_to_enum(colorspace, ColorSpace, error_cls=InvalidColorspaceError)

        # Build the palette data
        palette_data: dict[str, object] = {
            "colors": [],
            "palette_size": self.number_of_colors,
            "colorspace": colorspace,
        }

        colors_list = []
        # Add color data
        for color in self.colors:
            color_values = color.to(colorspace)
            color_data: dict[str, object] = {
                "frequency": float(color.frequency),
            }

            # Add colorspace-specific field
            colorspace_field = colorspace.value.lower()  # "rgb", "hsv", "hls"
            if colorspace == ColorSpace.RGB:
                # RGB values should be integers
                color_data[colorspace_field] = [int(v) if isinstance(v, np.integer) else v for v in color_values]
            else:
                # HSV/HLS values should be floats
                color_data[colorspace_field] = [
                    float(v) if isinstance(v, (np.integer, np.floating)) else v for v in color_values
                ]

            # Add hex (always present, derived from RGB)
            color_data["hex"] = color.hex

            # Add RGB reference if colorspace is not RGB
            if colorspace != ColorSpace.RGB:
                color_data["rgb"] = [int(v) if isinstance(v, np.integer) else v for v in color.rgb]

            colors_list.append(color_data)

        palette_data["colors"] = colors_list

        # Add metadata if requested and available
        if include_metadata and self.metadata:
            metadata_dict: dict[str, object] = {}

            if "image_source" in self.metadata:
                metadata_dict["image_source"] = self.metadata["image_source"]
            if "source_type" in self.metadata:
                metadata_dict["source_type"] = self.metadata["source_type"]
            if "extraction_params" in self.metadata:
                metadata_dict["extraction_params"] = self.metadata["extraction_params"]
            if "image_info" in self.metadata:
                metadata_dict["image_info"] = self.metadata["image_info"]
            if "processing_stats" in self.metadata:
                metadata_dict["processing_stats"] = self.metadata["processing_stats"]

            palette_data["metadata"] = metadata_dict

        # Save to file if filename provided
        if filename is not None:
            with open(filename, "w") as f:
                json.dump(palette_data, f, indent=2)
            return None

        # Return data if no filename provided
        return palette_data

    def export(
        self,
        filename: str,
        colorspace: ColorSpace | str = ColorSpace.RGB,
        include_metadata: bool = True,
    ) -> None:
        """
        Export palette to JSON format.

        Parameters:
            filename (str): File to save to (extension will be added automatically if not present).
            colorspace (ColorSpace | str): Color space to use (enum member, its
                value, or case-insensitive name).
            include_metadata (bool): Whether to include metadata.
        """

        # Add .json extension if not present
        if not filename.endswith(".json"):
            filename = f"{filename}.json"

        self.to_json(filename=filename, colorspace=colorspace, include_metadata=include_metadata)

    def __str__(self):
        return "".join(["({}, {}, {}, {}) \n".format(c.rgb[0], c.rgb[1], c.rgb[2], c.frequency) for c in self.colors])

    # Convenient metadata accessors
    @property
    def image_source(self) -> str | None:
        """Get the image source from metadata."""
        return self.metadata.get("image_source") if self.metadata else None

    @property
    def source_type(self) -> SourceType | None:
        """Get the source type from metadata."""
        return self.metadata.get("source_type") if self.metadata else None

    @property
    def extraction_params(self) -> ExtractionParams | None:
        """Get the extraction parameters from metadata."""
        return self.metadata.get("extraction_params") if self.metadata else None

    @property
    def image_info(self) -> ImageInfo | None:
        """Get the image information from metadata."""
        return self.metadata.get("image_info") if self.metadata else None

    @property
    def processing_stats(self) -> ProcessingStats | None:
        """Get the processing statistics from metadata."""
        return self.metadata.get("processing_stats") if self.metadata else None

extraction_params: ExtractionParams | None property

Get the extraction parameters from metadata.

image_info: ImageInfo | None property

Get the image information from metadata.

image_source: str | None property

Get the image source from metadata.

processing_stats: ProcessingStats | None property

Get the processing statistics from metadata.

source_type: SourceType | None property

Get the source type from metadata.

__init__

Initializes a color palette with a list of Color objects.

Parameters:

Name Type Description Default
colors list[Color]

A list of Color objects.

required
Note

For a palette produced by :func:~pylette.extract_colors, frequencies are the per-color relative weights and sum to 1.0.

Source code in pylette/src/palette.py
def __init__(self, colors: list[Color], metadata: PaletteMetaData | None = None):
    """
    Initializes a color palette with a list of Color objects.

    Parameters:
        colors (list[Color]): A list of Color objects.

    Note:
        For a palette produced by :func:`~pylette.extract_colors`,
        ``frequencies`` are the per-color relative weights and sum to ``1.0``.
    """

    self.colors = colors
    self.frequencies = [c.frequency for c in colors]
    self.number_of_colors = len(colors)
    self.metadata = metadata

dedup

Return a new palette with exactly-equal colors merged (frequencies summed).

Only colorspace-identical colors (same 8-bit RGB) are collapsed; for perceptual near-duplicates use :meth:merge_similar. The original palette is left unchanged.

Returns:

Name Type Description
Palette Palette

A new palette with exact duplicates removed.

Source code in pylette/src/palette.py
def dedup(self) -> "Palette":
    """Return a new palette with exactly-equal colors merged (frequencies summed).

    Only colorspace-identical colors (same 8-bit RGB) are collapsed; for
    perceptual near-duplicates use :meth:`merge_similar`. The original palette
    is left unchanged.

    Returns:
        Palette: A new palette with exact duplicates removed.
    """
    return Palette(operations.dedup(self.colors))

display

Displays the color palette as an image, with an option for saving the image.

Parameters:

Name Type Description Default
w int

Width of each color component.

50
h int

Height of each color component.

50
save_to_file bool

Whether to save the file or not.

False
filename str

Filename.

'color_palette'
extension str

File extension.

'jpg'
Source code in pylette/src/palette.py
def display(
    self,
    w: int = 50,
    h: int = 50,
    save_to_file: bool = False,
    filename: str = "color_palette",
    extension: str = "jpg",
) -> None:
    """
    Displays the color palette as an image, with an option for saving the image.

    Parameters:
        w (int): Width of each color component.
        h (int): Height of each color component.
        save_to_file (bool): Whether to save the file or not.
        filename (str): Filename.
        extension (str): File extension.
    """
    img = self._generate_palette_image(w, h)
    img.show()

    if save_to_file:
        self.save(w, h, filename, extension)

export

Export palette to JSON format.

Parameters:

Name Type Description Default
filename str

File to save to (extension will be added automatically if not present).

required
colorspace ColorSpace | str

Color space to use (enum member, its value, or case-insensitive name).

RGB
include_metadata bool

Whether to include metadata.

True
Source code in pylette/src/palette.py
def export(
    self,
    filename: str,
    colorspace: ColorSpace | str = ColorSpace.RGB,
    include_metadata: bool = True,
) -> None:
    """
    Export palette to JSON format.

    Parameters:
        filename (str): File to save to (extension will be added automatically if not present).
        colorspace (ColorSpace | str): Color space to use (enum member, its
            value, or case-insensitive name).
        include_metadata (bool): Whether to include metadata.
    """

    # Add .json extension if not present
    if not filename.endswith(".json"):
        filename = f"{filename}.json"

    self.to_json(filename=filename, colorspace=colorspace, include_metadata=include_metadata)

gradient

Return a new palette with interpolated colors bridging consecutive swatches.

Between each consecutive pair of colors, steps_between OKLab-interpolated colors are inserted, producing a smooth ramp across the whole palette. The result has equal frequencies summing to 1.0. A palette with fewer than two colors has nothing to bridge and is returned unchanged. The original palette is left unchanged.

Parameters:

Name Type Description Default
steps_between int

Colors to insert between each pair (>= 1).

required

Returns:

Name Type Description
Palette Palette

A new palette holding the gradient.

Raises:

Type Description
ValueError

If steps_between is less than 1.

Source code in pylette/src/palette.py
def gradient(self, steps_between: int) -> "Palette":
    """Return a new palette with interpolated colors bridging consecutive swatches.

    Between each consecutive pair of colors, ``steps_between`` OKLab-interpolated
    colors are inserted, producing a smooth ramp across the whole palette. The
    result has equal frequencies summing to 1.0. A palette with fewer than two
    colors has nothing to bridge and is returned unchanged. The original palette
    is left unchanged.

    Parameters:
        steps_between (int): Colors to insert between each pair (>= 1).

    Returns:
        Palette: A new palette holding the gradient.

    Raises:
        ValueError: If ``steps_between`` is less than 1.
    """
    if steps_between < 1:
        raise ValueError(f"steps_between must be at least 1, got {steps_between}.")
    colors = self.colors
    if len(colors) < 2:
        return Palette([Color.from_srgb_float(c.rgb_float, c.frequency, alpha=c.opacity) for c in colors])
    out: list[Color] = []
    for idx in range(len(colors) - 1):
        segment = operations.interpolate(colors[idx], colors[idx + 1], steps_between + 2)
        if idx > 0:
            segment = segment[1:]  # drop the seam color shared with the previous segment
        out.extend(segment)
    return Palette(operations.normalize_frequencies(out))

harmony

Return a color-harmony scheme seeded from this palette's dominant color.

Seeds from the dominant (highest-frequency) color and delegates to the harmony operation. An empty palette yields an empty palette.

Parameters:

Name Type Description Default
kind HarmonyKind | str

"complementary", "triadic", or "analogous".

required

Returns:

Name Type Description
Palette Palette

A new palette holding the harmony scheme.

Raises:

Type Description
InvalidHarmonyError

If kind is not recognized.

Source code in pylette/src/palette.py
def harmony(self, kind: HarmonyKind | str) -> "Palette":
    """Return a color-harmony scheme seeded from this palette's dominant color.

    Seeds from the dominant (highest-frequency) color and delegates to the
    harmony operation. An empty palette yields an empty palette.

    Parameters:
        kind (HarmonyKind | str): ``"complementary"``, ``"triadic"``, or
            ``"analogous"``.

    Returns:
        Palette: A new palette holding the harmony scheme.

    Raises:
        InvalidHarmonyError: If ``kind`` is not recognized.
    """
    if not self.colors:
        return Palette([])
    seed = max(self.colors, key=lambda c: c.frequency)
    return Palette(operations.harmony(seed, kind))

merge_similar

Return a new palette with perceptually similar colors merged.

Colors within delta_e of each other (OKLab ΔE, see :meth:Color.delta_e) collapse to their frequency-weighted OKLab mean, with frequencies summed (so the total stays ≈ 1.0). The original palette is left unchanged. For exact duplicates only, use :meth:dedup.

Parameters:

Name Type Description Default
delta_e float

Non-negative ΔE threshold; larger merges more.

required

Returns:

Name Type Description
Palette Palette

A new palette with near-duplicates merged.

Source code in pylette/src/palette.py
def merge_similar(self, delta_e: float) -> "Palette":
    """Return a new palette with perceptually similar colors merged.

    Colors within ``delta_e`` of each other (OKLab ΔE, see
    :meth:`Color.delta_e`) collapse to their frequency-weighted OKLab mean, with
    frequencies summed (so the total stays ≈ 1.0). The original palette is left
    unchanged. For exact duplicates only, use :meth:`dedup`.

    Parameters:
        delta_e (float): Non-negative ΔE threshold; larger merges more.

    Returns:
        Palette: A new palette with near-duplicates merged.
    """
    return Palette(operations.merge_similar(self.colors, delta_e))

random_color

Returns N random colors from the palette, either using the frequency of each color, or choosing uniformly.

Parameters:

Name Type Description Default
N int

Number of random colors to return.

required
mode str

Mode to use for selection. Can be "frequency" or "uniform".

'frequency'

Returns:

Type Description
list[Color]

list[Color]: List of N random colors from the palette.

Source code in pylette/src/palette.py
def random_color(self, N: int, mode: str = "frequency") -> list[Color]:
    """
    Returns N random colors from the palette, either using the frequency of each color, or choosing uniformly.

    Parameters:
        N (int): Number of random colors to return.
        mode (str): Mode to use for selection. Can be "frequency" or "uniform".

    Returns:
        list[Color]: List of N random colors from the palette.
    """

    if mode == "frequency":
        # Convert to numpy-compatible format for weighted selection
        colors_array = np.array(range(len(self.colors)))
        indices = np.random.choice(colors_array, size=N, p=self.frequencies)
        return [self.colors[i] for i in indices]
    elif mode == "uniform":
        # Uniform selection without weights
        colors_array = np.array(range(len(self.colors)))
        indices = np.random.choice(colors_array, size=N)
        return [self.colors[i] for i in indices]
    else:
        raise ValueError(f"Invalid mode: {mode}. Must be 'frequency' or 'uniform'.")

save

Saves the color palette as an image.

Parameters:

Name Type Description Default
w int

Width of each color component.

50
h int

Height of each color component.

50
filename str

Filename.

'color_palette'
extension str

File extension.

'jpg'
Source code in pylette/src/palette.py
def save(
    self,
    w: int = 50,
    h: int = 50,
    filename: str = "color_palette",
    extension: str = "jpg",
) -> None:
    """
    Saves the color palette as an image.

    Parameters:
        w (int): Width of each color component.
        h (int): Height of each color component.
        filename (str): Filename.
        extension (str): File extension.
    """
    img = self._generate_palette_image(w, h)
    img.save(f"{filename}.{extension}")

sort_perceptual

Return a new palette sorted by perceptual lightness (OKLab L).

Ascending by default (darkest first). The sort is stable and idempotent. The original palette is left unchanged.

Parameters:

Name Type Description Default
descending bool

If True, sort lightest first.

False

Returns:

Name Type Description
Palette Palette

A new, perceptually sorted palette.

Source code in pylette/src/palette.py
def sort_perceptual(self, descending: bool = False) -> "Palette":
    """Return a new palette sorted by perceptual lightness (OKLab L).

    Ascending by default (darkest first). The sort is stable and idempotent.
    The original palette is left unchanged.

    Parameters:
        descending (bool): If True, sort lightest first.

    Returns:
        Palette: A new, perceptually sorted palette.
    """
    return Palette(operations.sort_perceptual(self.colors, descending=descending))

to_json

Exports the palette to JSON format.

Parameters:

Name Type Description Default
filename str | None

File to save to. If None, returns the dictionary.

None
colorspace ColorSpace | str

Color space to use (enum member, its value, or case-insensitive name).

RGB
include_metadata bool

Whether to include palette metadata.

True

Returns:

Type Description
dict[str, object] | None

dict | None: The palette data as a dictionary if filename is None.

Source code in pylette/src/palette.py
def to_json(
    self,
    filename: str | None = None,
    colorspace: ColorSpace | str = ColorSpace.RGB,
    include_metadata: bool = True,
) -> dict[str, object] | None:
    """
    Exports the palette to JSON format.

    Parameters:
        filename (str | None): File to save to. If None, returns the dictionary.
        colorspace (ColorSpace | str): Color space to use (enum member, its
            value, or case-insensitive name).
        include_metadata (bool): Whether to include palette metadata.

    Returns:
        dict | None: The palette data as a dictionary if filename is None.
    """

    colorspace = coerce_to_enum(colorspace, ColorSpace, error_cls=InvalidColorspaceError)

    # Build the palette data
    palette_data: dict[str, object] = {
        "colors": [],
        "palette_size": self.number_of_colors,
        "colorspace": colorspace,
    }

    colors_list = []
    # Add color data
    for color in self.colors:
        color_values = color.to(colorspace)
        color_data: dict[str, object] = {
            "frequency": float(color.frequency),
        }

        # Add colorspace-specific field
        colorspace_field = colorspace.value.lower()  # "rgb", "hsv", "hls"
        if colorspace == ColorSpace.RGB:
            # RGB values should be integers
            color_data[colorspace_field] = [int(v) if isinstance(v, np.integer) else v for v in color_values]
        else:
            # HSV/HLS values should be floats
            color_data[colorspace_field] = [
                float(v) if isinstance(v, (np.integer, np.floating)) else v for v in color_values
            ]

        # Add hex (always present, derived from RGB)
        color_data["hex"] = color.hex

        # Add RGB reference if colorspace is not RGB
        if colorspace != ColorSpace.RGB:
            color_data["rgb"] = [int(v) if isinstance(v, np.integer) else v for v in color.rgb]

        colors_list.append(color_data)

    palette_data["colors"] = colors_list

    # Add metadata if requested and available
    if include_metadata and self.metadata:
        metadata_dict: dict[str, object] = {}

        if "image_source" in self.metadata:
            metadata_dict["image_source"] = self.metadata["image_source"]
        if "source_type" in self.metadata:
            metadata_dict["source_type"] = self.metadata["source_type"]
        if "extraction_params" in self.metadata:
            metadata_dict["extraction_params"] = self.metadata["extraction_params"]
        if "image_info" in self.metadata:
            metadata_dict["image_info"] = self.metadata["image_info"]
        if "processing_stats" in self.metadata:
            metadata_dict["processing_stats"] = self.metadata["processing_stats"]

        palette_data["metadata"] = metadata_dict

    # Save to file if filename provided
    if filename is not None:
        with open(filename, "w") as f:
            json.dump(palette_data, f, indent=2)
        return None

    # Return data if no filename provided
    return palette_data

pylette.Color

A single palette color.

The canonical representation is float sRGB in [0, 1] (plus a float alpha). 8-bit quantization happens only at the output boundaries (:attr:rgb, :attr:rgba, :attr:a, :attr:hex), so colors constructed from continuous centroids keep their precision until they are read out.

Source code in pylette/src/color.py
class Color(object):
    """A single palette color.

    The canonical representation is float sRGB in ``[0, 1]`` (plus a float
    alpha). 8-bit quantization happens only at the output boundaries
    (:attr:`rgb`, :attr:`rgba`, :attr:`a`, :attr:`hex`), so colors constructed
    from continuous centroids keep their precision until they are read out.
    """

    def __init__(self, rgba: tuple[int, ...], frequency: float):
        """
        Initializes a Color object from 8-bit RGBA values.

        The 8-bit input is the quantized view of the color; it is converted to
        the canonical float store on construction.

        Parameters:
            rgba (tuple[int, ...]): A tuple of RGBA values, each in [0, 255].
            frequency (float): The frequency of the color.
        """
        assert len(rgba) == 4, "RGBA values must be a tuple of length 4"
        r, g, b, alpha = (int(round(float(v))) for v in rgba)
        self._srgb: tuple[float, float, float] = (r / 255.0, g / 255.0, b / 255.0)
        self._alpha: float = alpha / 255.0
        self.frequency: float = frequency

    @classmethod
    def from_srgb_float(
        cls,
        srgb: tuple[float, float, float],
        frequency: float,
        alpha: float = 1.0,
    ) -> "Color":
        """
        Constructs a Color from float sRGB components in ``[0, 1]``.

        This is the precision-preserving entry point for extractors whose
        centroids live in continuous space (e.g. OKLab); it avoids the round
        trip through 8-bit that :meth:`__init__` performs. Components are
        clamped into ``[0, 1]`` so out-of-gamut centroids are handled gracefully.

        Parameters:
            srgb (tuple[float, float, float]): Gamma-encoded sRGB components.
            frequency (float): The frequency of the color.
            alpha (float): Alpha in ``[0, 1]`` (default fully opaque).

        Returns:
            Color: A color whose canonical store holds the given floats.
        """
        obj = cls.__new__(cls)
        r, g, b = srgb
        obj._srgb = (_clamp_unit(r), _clamp_unit(g), _clamp_unit(b))
        obj._alpha = _clamp_unit(alpha)
        obj.frequency = frequency
        return obj

    @property
    def rgb_float(self) -> tuple[float, float, float]:
        """
        The canonical color as float sRGB components in ``[0, 1]``.

        Returns:
            tuple[float, float, float]: The (r, g, b) components.
        """
        return self._srgb

    @property
    def rgb(self) -> tuple[int, int, int]:
        """
        The color as 8-bit sRGB.

        Returns:
            tuple[int, int, int]: (r, g, b) as plain Python ints in [0, 255].
        """
        r, g, b = self._srgb
        return (int(round(r * 255.0)), int(round(g * 255.0)), int(round(b * 255.0)))

    @property
    def alpha(self) -> int:
        """
        The alpha channel as a raw 8-bit value (matches :attr:`rgba`).

        Returns:
            int: Alpha as a plain Python int in [0, 255].
        """
        return int(round(self._alpha * 255.0))

    @property
    def opacity(self) -> float:
        """
        The alpha channel as a fraction (opacity) in ``[0, 1]``.

        Returns:
            float: Opacity in [0, 1].
        """
        return self._alpha

    @property
    def rgba(self) -> tuple[int, int, int, int]:
        """
        The color as 8-bit RGBA.

        Returns:
            tuple[int, int, int, int]: (r, g, b, a) as plain Python ints in [0, 255].
        """
        r, g, b = self.rgb
        return (r, g, b, self.alpha)

    @property
    def a(self) -> int:
        """Deprecated alias for :attr:`alpha`."""
        warnings.warn(
            "Color.a is deprecated and will be removed; use Color.alpha instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.alpha

    @property
    def weight(self) -> float:
        """Deprecated alias for :attr:`opacity`.

        The name is misleading: in a palette context "weight" reads as relative
        importance (frequency), but it holds opacity. Use :attr:`opacity`.
        """
        warnings.warn(
            "Color.weight is deprecated and will be removed; use Color.opacity instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.opacity

    @property
    def freq(self) -> float:
        """Deprecated alias for :attr:`frequency`."""
        warnings.warn(
            "Color.freq is deprecated and will be removed; use Color.frequency instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.frequency

    def display(self, w: int = 50, h: int = 50) -> None:
        """
        Displays the color in a window of specified width and height.

        Parameters:
        w (int): Width of the window in pixels.
        h (int): Height of the window in pixels.
        """

        from PIL import Image

        img = Image.new("RGBA", size=(w, h), color=self.rgba)
        img.show()

    def __lt__(self, other: "Color") -> bool:
        """
        Compares the frequency of this color with another color.

        Parameters:
            other (Color): The other Color object to compare with.

        Returns:
            bool: True if the frequency of this color is less than the frequency of the other color, False otherwise.
        """
        return self.frequency < other.frequency

    def to(self, space: ColorSpace | str = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]:
        """
        Returns the color in the requested color space.

        This is the single conversion entry point; ``get_colors``, ``hsv``,
        ``hls``, and the OKLab view all route through it, and all space math
        lives in :mod:`pylette.src.colorspaces`.

        Parameters:
            space (ColorSpace | str): The target space (enum member, its value,
                or case-insensitive name): ``rgb``, ``hsv``, ``hls``, or ``oklab``.

        Returns:
            tuple[int, ...] | tuple[float, ...]: The color values in that space
            (RGB as 8-bit ints; the others as floats).
        """
        space = coerce_to_enum(space, ColorSpace, error_cls=InvalidColorspaceError)
        if space is ColorSpace.RGB:
            return self.rgb
        if space is ColorSpace.HSV:
            return colorsys.rgb_to_hsv(*self._srgb)
        if space is ColorSpace.HLS:
            return colorsys.rgb_to_hls(*self._srgb)
        return srgb_to_oklab(self._srgb)

    def delta_e(self, other: "Color") -> float:
        """Perceptual color difference to ``other`` as Euclidean distance in OKLab.

        OKLab is built so that straight-line distance approximates perceived
        difference, so no extra weighting is applied. Symmetric, non-negative,
        and zero for colors with identical OKLab coordinates.

        Parameters:
            other (Color): The color to compare against.

        Returns:
            float: The OKLab ΔE between the two colors.
        """
        return float(np.linalg.norm(np.array(self.oklab) - np.array(other.oklab)))

    def get_colors(self, colorspace: ColorSpace | str = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]:
        """
        Deprecated alias for :meth:`to`.

        Parameters:
            colorspace (ColorSpace | str): The color space to use (enum member,
                its value, or case-insensitive name).

        Returns:
            tuple[int, ...] | tuple[float, ...]: The color values in the specified color space.
        """
        warnings.warn(
            "Color.get_colors() is deprecated and will be removed; use Color.to() instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.to(colorspace)

    @property
    def hsv(self) -> tuple[float, float, float]:
        """
        Converts the color to HSV color space, derived from the canonical float store.

        Returns:
            tuple[float, float, float]: The color values in HSV color space.
        """
        return cast("tuple[float, float, float]", self.to(ColorSpace.HSV))

    @property
    def hls(self) -> tuple[float, float, float]:
        """
        Converts the color to HLS color space, derived from the canonical float store.

        Returns:
            tuple[float, float, float]: The color values in HLS color space.
        """
        return cast("tuple[float, float, float]", self.to(ColorSpace.HLS))

    @property
    def oklab(self) -> tuple[float, float, float]:
        """
        Converts the color to the perceptual OKLab color space.

        Returns:
            tuple[float, float, float]: The ``(L, a, b)`` OKLab components.
        """
        return cast("tuple[float, float, float]", self.to(ColorSpace.OKLAB))

    @property
    def hex(self) -> str:
        """
        Returns the color as a hexadecimal string.

        Returns:
            str: The color in hexadecimal format (e.g., "#FF5733").
        """
        r, g, b = self.rgb
        return f"#{r:02X}{g:02X}{b:02X}"

    def gradient_to(self, other: "Color", steps: int) -> "Palette":
        """Return a palette of ``steps`` colors interpolated from this color to ``other``.

        Interpolation runs in OKLab for a perceptually even ramp and includes both
        endpoints. The generated colors get equal frequencies summing to 1.0.

        Parameters:
            other (Color): The end color of the ramp.
            steps (int): Total number of colors, including both endpoints (>= 2).

        Returns:
            Palette: A new palette holding the gradient.
        """
        from pylette.src import operations
        from pylette.src.palette import Palette

        return Palette(operations.interpolate(self, other, steps))

    def harmony(self, kind: HarmonyKind | str) -> "Palette":
        """Return a color-harmony scheme generated from this color.

        ``kind`` is ``"complementary"``, ``"triadic"``, or ``"analogous"`` (a
        :class:`HarmonyKind` member, its value, or case-insensitive name). The
        returned palette holds the seed plus its hue-rotated partners with equal
        frequencies.

        Parameters:
            kind (HarmonyKind | str): The harmony to generate.

        Returns:
            Palette: A new palette holding the harmony scheme.

        Raises:
            InvalidHarmonyError: If ``kind`` is not recognized.
        """
        from pylette.src import operations
        from pylette.src.palette import Palette

        return Palette(operations.harmony(self, kind))

    @property
    def luminance(self) -> float:
        """
        Calculates the luminance of the color, derived from the canonical float store.

        Returns:
        float: The luminance of the color, on the same 0-255 scale as the 8-bit channels.
        """
        return float(np.dot(luminance_weights, self._srgb)) * 255.0

a: int property

Deprecated alias for :attr:alpha.

alpha: int property

The alpha channel as a raw 8-bit value (matches :attr:rgba).

Returns:

Name Type Description
int int

Alpha as a plain Python int in [0, 255].

freq: float property

Deprecated alias for :attr:frequency.

hex: str property

Returns the color as a hexadecimal string.

Returns:

Name Type Description
str str

The color in hexadecimal format (e.g., "#FF5733").

hls: tuple[float, float, float] property

Converts the color to HLS color space, derived from the canonical float store.

Returns:

Type Description
tuple[float, float, float]

tuple[float, float, float]: The color values in HLS color space.

hsv: tuple[float, float, float] property

Converts the color to HSV color space, derived from the canonical float store.

Returns:

Type Description
tuple[float, float, float]

tuple[float, float, float]: The color values in HSV color space.

luminance: float property

Calculates the luminance of the color, derived from the canonical float store.

Returns: float: The luminance of the color, on the same 0-255 scale as the 8-bit channels.

oklab: tuple[float, float, float] property

Converts the color to the perceptual OKLab color space.

Returns:

Type Description
tuple[float, float, float]

tuple[float, float, float]: The (L, a, b) OKLab components.

opacity: float property

The alpha channel as a fraction (opacity) in [0, 1].

Returns:

Name Type Description
float float

Opacity in [0, 1].

rgb: tuple[int, int, int] property

The color as 8-bit sRGB.

Returns:

Type Description
tuple[int, int, int]

tuple[int, int, int]: (r, g, b) as plain Python ints in [0, 255].

rgb_float: tuple[float, float, float] property

The canonical color as float sRGB components in [0, 1].

Returns:

Type Description
tuple[float, float, float]

tuple[float, float, float]: The (r, g, b) components.

rgba: tuple[int, int, int, int] property

The color as 8-bit RGBA.

Returns:

Type Description
tuple[int, int, int, int]

tuple[int, int, int, int]: (r, g, b, a) as plain Python ints in [0, 255].

weight: float property

Deprecated alias for :attr:opacity.

The name is misleading: in a palette context "weight" reads as relative importance (frequency), but it holds opacity. Use :attr:opacity.

__init__

Initializes a Color object from 8-bit RGBA values.

The 8-bit input is the quantized view of the color; it is converted to the canonical float store on construction.

Parameters:

Name Type Description Default
rgba tuple[int, ...]

A tuple of RGBA values, each in [0, 255].

required
frequency float

The frequency of the color.

required
Source code in pylette/src/color.py
def __init__(self, rgba: tuple[int, ...], frequency: float):
    """
    Initializes a Color object from 8-bit RGBA values.

    The 8-bit input is the quantized view of the color; it is converted to
    the canonical float store on construction.

    Parameters:
        rgba (tuple[int, ...]): A tuple of RGBA values, each in [0, 255].
        frequency (float): The frequency of the color.
    """
    assert len(rgba) == 4, "RGBA values must be a tuple of length 4"
    r, g, b, alpha = (int(round(float(v))) for v in rgba)
    self._srgb: tuple[float, float, float] = (r / 255.0, g / 255.0, b / 255.0)
    self._alpha: float = alpha / 255.0
    self.frequency: float = frequency

__lt__

Compares the frequency of this color with another color.

Parameters:

Name Type Description Default
other Color

The other Color object to compare with.

required

Returns:

Name Type Description
bool bool

True if the frequency of this color is less than the frequency of the other color, False otherwise.

Source code in pylette/src/color.py
def __lt__(self, other: "Color") -> bool:
    """
    Compares the frequency of this color with another color.

    Parameters:
        other (Color): The other Color object to compare with.

    Returns:
        bool: True if the frequency of this color is less than the frequency of the other color, False otherwise.
    """
    return self.frequency < other.frequency

delta_e

Perceptual color difference to other as Euclidean distance in OKLab.

OKLab is built so that straight-line distance approximates perceived difference, so no extra weighting is applied. Symmetric, non-negative, and zero for colors with identical OKLab coordinates.

Parameters:

Name Type Description Default
other Color

The color to compare against.

required

Returns:

Name Type Description
float float

The OKLab ΔE between the two colors.

Source code in pylette/src/color.py
def delta_e(self, other: "Color") -> float:
    """Perceptual color difference to ``other`` as Euclidean distance in OKLab.

    OKLab is built so that straight-line distance approximates perceived
    difference, so no extra weighting is applied. Symmetric, non-negative,
    and zero for colors with identical OKLab coordinates.

    Parameters:
        other (Color): The color to compare against.

    Returns:
        float: The OKLab ΔE between the two colors.
    """
    return float(np.linalg.norm(np.array(self.oklab) - np.array(other.oklab)))

display

Displays the color in a window of specified width and height.

Parameters: w (int): Width of the window in pixels. h (int): Height of the window in pixels.

Source code in pylette/src/color.py
def display(self, w: int = 50, h: int = 50) -> None:
    """
    Displays the color in a window of specified width and height.

    Parameters:
    w (int): Width of the window in pixels.
    h (int): Height of the window in pixels.
    """

    from PIL import Image

    img = Image.new("RGBA", size=(w, h), color=self.rgba)
    img.show()

from_srgb_float classmethod

Constructs a Color from float sRGB components in [0, 1].

This is the precision-preserving entry point for extractors whose centroids live in continuous space (e.g. OKLab); it avoids the round trip through 8-bit that :meth:__init__ performs. Components are clamped into [0, 1] so out-of-gamut centroids are handled gracefully.

Parameters:

Name Type Description Default
srgb tuple[float, float, float]

Gamma-encoded sRGB components.

required
frequency float

The frequency of the color.

required
alpha float

Alpha in [0, 1] (default fully opaque).

1.0

Returns:

Name Type Description
Color Color

A color whose canonical store holds the given floats.

Source code in pylette/src/color.py
@classmethod
def from_srgb_float(
    cls,
    srgb: tuple[float, float, float],
    frequency: float,
    alpha: float = 1.0,
) -> "Color":
    """
    Constructs a Color from float sRGB components in ``[0, 1]``.

    This is the precision-preserving entry point for extractors whose
    centroids live in continuous space (e.g. OKLab); it avoids the round
    trip through 8-bit that :meth:`__init__` performs. Components are
    clamped into ``[0, 1]`` so out-of-gamut centroids are handled gracefully.

    Parameters:
        srgb (tuple[float, float, float]): Gamma-encoded sRGB components.
        frequency (float): The frequency of the color.
        alpha (float): Alpha in ``[0, 1]`` (default fully opaque).

    Returns:
        Color: A color whose canonical store holds the given floats.
    """
    obj = cls.__new__(cls)
    r, g, b = srgb
    obj._srgb = (_clamp_unit(r), _clamp_unit(g), _clamp_unit(b))
    obj._alpha = _clamp_unit(alpha)
    obj.frequency = frequency
    return obj

get_colors

Deprecated alias for :meth:to.

Parameters:

Name Type Description Default
colorspace ColorSpace | str

The color space to use (enum member, its value, or case-insensitive name).

RGB

Returns:

Type Description
tuple[int, ...] | tuple[float, ...]

tuple[int, ...] | tuple[float, ...]: The color values in the specified color space.

Source code in pylette/src/color.py
def get_colors(self, colorspace: ColorSpace | str = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]:
    """
    Deprecated alias for :meth:`to`.

    Parameters:
        colorspace (ColorSpace | str): The color space to use (enum member,
            its value, or case-insensitive name).

    Returns:
        tuple[int, ...] | tuple[float, ...]: The color values in the specified color space.
    """
    warnings.warn(
        "Color.get_colors() is deprecated and will be removed; use Color.to() instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return self.to(colorspace)

gradient_to

Return a palette of steps colors interpolated from this color to other.

Interpolation runs in OKLab for a perceptually even ramp and includes both endpoints. The generated colors get equal frequencies summing to 1.0.

Parameters:

Name Type Description Default
other Color

The end color of the ramp.

required
steps int

Total number of colors, including both endpoints (>= 2).

required

Returns:

Name Type Description
Palette Palette

A new palette holding the gradient.

Source code in pylette/src/color.py
def gradient_to(self, other: "Color", steps: int) -> "Palette":
    """Return a palette of ``steps`` colors interpolated from this color to ``other``.

    Interpolation runs in OKLab for a perceptually even ramp and includes both
    endpoints. The generated colors get equal frequencies summing to 1.0.

    Parameters:
        other (Color): The end color of the ramp.
        steps (int): Total number of colors, including both endpoints (>= 2).

    Returns:
        Palette: A new palette holding the gradient.
    """
    from pylette.src import operations
    from pylette.src.palette import Palette

    return Palette(operations.interpolate(self, other, steps))

harmony

Return a color-harmony scheme generated from this color.

kind is "complementary", "triadic", or "analogous" (a :class:HarmonyKind member, its value, or case-insensitive name). The returned palette holds the seed plus its hue-rotated partners with equal frequencies.

Parameters:

Name Type Description Default
kind HarmonyKind | str

The harmony to generate.

required

Returns:

Name Type Description
Palette Palette

A new palette holding the harmony scheme.

Raises:

Type Description
InvalidHarmonyError

If kind is not recognized.

Source code in pylette/src/color.py
def harmony(self, kind: HarmonyKind | str) -> "Palette":
    """Return a color-harmony scheme generated from this color.

    ``kind`` is ``"complementary"``, ``"triadic"``, or ``"analogous"`` (a
    :class:`HarmonyKind` member, its value, or case-insensitive name). The
    returned palette holds the seed plus its hue-rotated partners with equal
    frequencies.

    Parameters:
        kind (HarmonyKind | str): The harmony to generate.

    Returns:
        Palette: A new palette holding the harmony scheme.

    Raises:
        InvalidHarmonyError: If ``kind`` is not recognized.
    """
    from pylette.src import operations
    from pylette.src.palette import Palette

    return Palette(operations.harmony(self, kind))

to

Returns the color in the requested color space.

This is the single conversion entry point; get_colors, hsv, hls, and the OKLab view all route through it, and all space math lives in :mod:pylette.src.colorspaces.

Parameters:

Name Type Description Default
space ColorSpace | str

The target space (enum member, its value, or case-insensitive name): rgb, hsv, hls, or oklab.

RGB

Returns:

Type Description
tuple[int, ...] | tuple[float, ...]

tuple[int, ...] | tuple[float, ...]: The color values in that space

tuple[int, ...] | tuple[float, ...]

(RGB as 8-bit ints; the others as floats).

Source code in pylette/src/color.py
def to(self, space: ColorSpace | str = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]:
    """
    Returns the color in the requested color space.

    This is the single conversion entry point; ``get_colors``, ``hsv``,
    ``hls``, and the OKLab view all route through it, and all space math
    lives in :mod:`pylette.src.colorspaces`.

    Parameters:
        space (ColorSpace | str): The target space (enum member, its value,
            or case-insensitive name): ``rgb``, ``hsv``, ``hls``, or ``oklab``.

    Returns:
        tuple[int, ...] | tuple[float, ...]: The color values in that space
        (RGB as 8-bit ints; the others as floats).
    """
    space = coerce_to_enum(space, ColorSpace, error_cls=InvalidColorspaceError)
    if space is ColorSpace.RGB:
        return self.rgb
    if space is ColorSpace.HSV:
        return colorsys.rgb_to_hsv(*self._srgb)
    if space is ColorSpace.HLS:
        return colorsys.rgb_to_hls(*self._srgb)
    return srgb_to_oklab(self._srgb)

Exceptions

Every error Pylette raises derives from PyletteError, so you can catch any Pylette-originated failure with a single except pylette.PyletteError and branch on the concrete subclass to identify the failure mode. Each subclass also derives from ValueError, so existing except ValueError handlers keep working.

pylette.PyletteError

Base class for every error raised by Pylette.

Source code in pylette/src/exceptions.py
class PyletteError(Exception):
    """Base class for every error raised by Pylette."""

pylette.InvalidImageError

An input image could not be loaded, or its type is unsupported.

Source code in pylette/src/exceptions.py
class InvalidImageError(PyletteError, ValueError):
    """An input image could not be loaded, or its type is unsupported."""

pylette.NoValidPixelsError

No pixels remain to extract a palette from (e.g. a fully alpha-masked image).

Source code in pylette/src/exceptions.py
class NoValidPixelsError(PyletteError, ValueError):
    """No pixels remain to extract a palette from (e.g. a fully alpha-masked image)."""

pylette.UnknownExtractionMethodError

The requested extraction method is not a registered/known method.

Source code in pylette/src/exceptions.py
class UnknownExtractionMethodError(PyletteError, ValueError):
    """The requested extraction method is not a registered/known method."""

pylette.InvalidColorspaceError

The requested color space is not recognized.

Source code in pylette/src/exceptions.py
class InvalidColorspaceError(PyletteError, ValueError):
    """The requested color space is not recognized."""

Core Types:

pylette.types.ArrayImage: TypeAlias = NDArray[np.uint8] module-attribute

pylette.types.ArrayLike

Protocol for array-like objects.

Source code in pylette/src/types.py
class ArrayLike(Protocol):
    """Protocol for array-like objects."""

    def __array__(self) -> NDArray[np.uint8]: ...

pylette.types.BatchResult dataclass

Source code in pylette/src/types.py
@dataclass
class BatchResult:
    source: ImageInput
    result: "Palette | None" = None
    exception: Exception | None = None

    @property
    def success(self) -> bool:
        return self.result is not None

    @property
    def palette(self) -> "Palette | None":
        return self.result

    @property
    def error(self) -> "Exception | None":
        return self.exception

pylette.types.BytesImage: TypeAlias = bytes module-attribute

pylette.types.ColorArray: TypeAlias = NDArray[np.uint8] module-attribute

pylette.types.ColorSpace

Source code in pylette/src/types.py
class ColorSpace(str, Enum):
    RGB = "rgb"
    HSV = "hsv"
    HLS = "hls"
    OKLAB = "oklab"

pylette.types.ColorTuple: TypeAlias = RGBTuple | RGBATuple module-attribute

pylette.types.CV2Image: TypeAlias = MatLike module-attribute

pylette.types.ExtractionMethod

Source code in pylette/src/types.py
class ExtractionMethod(str, Enum):
    MC = "MedianCut"
    KM = "KMeans"
    OKLAB = "OKLab"

pylette.types.ExtractionParams

Source code in pylette/src/types.py
class ExtractionParams(TypedDict):
    palette_size: int
    mode: ExtractionMethod
    sort_mode: str | None
    resize: int | None
    alpha_mask_threshold: int | None

pylette.types.FloatArray: TypeAlias = NDArray[np.floating[Any]] module-attribute

pylette.types.ImageInfo

Source code in pylette/src/types.py
class ImageInfo(TypedDict):
    original_size: tuple[int, int]
    processed_size: tuple[int, int]
    format: str | None
    mode: str
    has_alpha: bool

pylette.types.ImageInput: TypeAlias = PathLikeImage | URLImage | BytesImage | ArrayImage | PILImage | CV2Image module-attribute

pylette.types.ImageLike

Protocol for image-like objects that can be converted to PIL Image.

Source code in pylette/src/types.py
class ImageLike(Protocol):
    """Protocol for image-like objects that can be converted to PIL Image."""

    pass

pylette.types.IntArray: TypeAlias = NDArray[np.integer[Any]] module-attribute

pylette.types.PaletteMetaData

Source code in pylette/src/types.py
class PaletteMetaData(TypedDict):
    image_source: str
    source_type: SourceType
    extraction_params: ExtractionParams
    image_info: ImageInfo
    processing_stats: ProcessingStats

pylette.types.PathLikeImage: TypeAlias = str | Path module-attribute

pylette.types.PILImage: TypeAlias = Image.Image module-attribute

pylette.types.ProcessingStats

Source code in pylette/src/types.py
class ProcessingStats(TypedDict):
    total_pixels: int
    valid_pixels: int
    extraction_time: float | None
    timestamp: str

pylette.types.RGBATuple: TypeAlias = tuple[int, int, int, int] module-attribute

pylette.types.RGBTuple: TypeAlias = tuple[int, int, int] module-attribute

pylette.types.SourceType

Source code in pylette/src/types.py
class SourceType(str, Enum):
    FILE_PATH = "file_path"
    URL = "url"
    BYTES = "bytes"
    PIL_IMAGE = "pil_image"
    NUMPY_ARRAY = "numpy_array"
    CV2_IMAGE = "cv2_image"
    UNKNOWN = "unknown"

pylette.types.URLImage: TypeAlias = str module-attribute