Skip to content

🎨 nc: Named colors in Python 🎨

The module nc collects named colors in Python.

There are two ways to use nc.

The simple way is as an intuitive and forgiving interface to a collection of over 2000 named colors, put together from almost 20 color palettes scraped from the Internet.

In the simplest use, it's a collection of about 1700 colors, some scraped from the Wikipedia (which includes some very strange colors), with a neat API.

For more precise use, color collections can be put together from schemes built into nc (currently html, juce, pwg, wikipedia, x11), or from custom color schemes created by the user.

There is also a collection of color swatches for the default color collection.

Examples

import nc

for c in nc.red, nc.light_green, nc.DarkGrey, nc['PUCE']:
    print(c, '=', *c)

# Prints:
#   Red = 255 0 0
#   Light green = 144 238 144
#   Dark grey = 85 85 85
#   Puce = 204 136 153

# Colors have red, green, blue or r, g, b components
assert nc.yellow.red == nc.yellow.r == 0
assert nc.yellow.green == nc.yellow.g == 255
assert nc.yellow.blue == nc.yellow.b == 255

# Lots of ways to reach colors
assert nc.black == nc(0, 0, 0) == nc('0, 0, 0') == nc('(0, 0, 0)') == nc(0)

# ``nc`` looks like a dict
assert nc.red == nc['red'] == nc['RED']
for name, color in nc.items():
    print(name, '=', *color)

# Prints:
#   Absolute Zero = 0 72 186
#   Acid green = 176 191 26
#   Aero = 124 185 232
#   ... many more

# closest() function

from random import randrange
for i in range(8):
    c1 = randrange(256), randrange(256), randrange(256)
    c2 = nc.closest(c1)
    print(c1, 'is closest to', c2, *c2)

# Prints:
#   (193, 207, 185) is closest to Honeydew 3 = 193 205 193
#   (181, 162, 188) is closest to Lilac = 200 162 200
#   (122, 110, 250) is closest to Slate blue 1 = 131 111 255
#   (56, 218, 180) is closest to Turquoise = 64 224 208

API Documentation

NC

Source code in nc/__init__.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class NC:
    colors = _colors
    Colors = _colors.Colors

    @cached_property
    def COLORS(self) -> _colors.Colors:
        """The underlying instance of `nc.colors.Colors with all the colors"""
        return self.Colors(*_DEFAULT_PALETTES)

    def __getattr__(self, name) -> _colors.Colors:
        """Gets a color as a name attribute"""
        try:
            return globals()[name]
        except KeyError:
            pass
        return getattr(self.COLORS, name)

    def __call__(self, *args, **kwargs) -> _colors.Colors:
        """"""
        return self.COLORS(*args, **kwargs)

    def __getitem__(self, name) -> _colors.Colors:
        """Gets a color by name"""
        return self.COLORS[name]

    def __contains__(self, x) -> bool:
        """Return true if this string name, tuple or color is in this list"""
        return x in self.COLORS

    def __len__(self) -> int:
        """Returns the number of colors"""
        return len(self.COLORS)

    def __iter__(self) -> Iterator[_colors.Colors]:
        """Iterate over all the colors in alphabetical order"""
        return iter(self.COLORS)

COLORS: _colors.Colors cached property

The underlying instance of `nc.colors.Colors with all the colors

__contains__(x)

Return true if this string name, tuple or color is in this list

Source code in nc/__init__.py
105
106
107
def __contains__(self, x) -> bool:
    """Return true if this string name, tuple or color is in this list"""
    return x in self.COLORS

__getattr__(name)

Gets a color as a name attribute

Source code in nc/__init__.py
89
90
91
92
93
94
95
def __getattr__(self, name) -> _colors.Colors:
    """Gets a color as a name attribute"""
    try:
        return globals()[name]
    except KeyError:
        pass
    return getattr(self.COLORS, name)

__getitem__(name)

Gets a color by name

Source code in nc/__init__.py
101
102
103
def __getitem__(self, name) -> _colors.Colors:
    """Gets a color by name"""
    return self.COLORS[name]

__iter__()

Iterate over all the colors in alphabetical order

Source code in nc/__init__.py
113
114
115
def __iter__(self) -> Iterator[_colors.Colors]:
    """Iterate over all the colors in alphabetical order"""
    return iter(self.COLORS)

__len__()

Returns the number of colors

Source code in nc/__init__.py
109
110
111
def __len__(self) -> int:
    """Returns the number of colors"""
    return len(self.COLORS)

Color

Bases: COLOR_TUPLE

A single Color, represented as a named triple of integers in the range [0, 256).

Source code in nc/color.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class Color(COLOR_TUPLE):
    """A single Color, represented as a named triple of integers in the range
    [0, 256).
    """

    COLORS = None
    GAMMA = 2.5

    def __new__(cls, *args):
        return super().__new__(cls, *_make(cls, args))

    def __str__(self):
        return self.COLORS._rgb_to_name.get(self) or '({}, {}, {})'.format(
            *self
        )

    def __repr__(self):
        name = str(self)
        if not name.startswith('('):
            return "Color('%s')" % name
        return 'Color' + name

    def closest(self) -> Self:
        """
        Return the closest named color to `self`.  This is quite slow,
        particularly in large schemes.
        """
        return self.COLORS.closest(self)

    def distance2(self, other) -> int:
        """Return the square of the distance between this and another color"""
        d = (i - j for i, j in zip(self, other))
        return sum(i * i for i in d)

    def distance(self, other) -> float:
        """Return the distance between this and another color"""
        return math.sqrt(self.distance2(other))

    @cached_property
    def rgb(self) -> int:
        """Return an integer between 0 and 0xFFFFFF combining the components"""
        return self.r * 0x10000 + self.g * 0x100 + self.b

    @cached_property
    def brightness(self) -> float:
        """gamma-weighted average of intensities"""
        return (sum(c ** self.GAMMA for c in self) / 3) ** (1 / self.GAMMA)

    @cached_property
    def hsl(self) -> Tuple[int, int, int]:
        return colorsys.rgb_to_hsl(*self._to())

    @cached_property
    def hsv(self) -> Tuple[int, int, int]:
        return colorsys.rgb_to_hsv(*self._to())

    @cached_property
    def yiq(self) -> Tuple[int, int, int]:
        return colorsys.rgb_to_yiq(*self._to())

    @classmethod
    def from_hsl(cls, h, s, l) -> Self:  # noqa E741
        return cls._from(colorsys.hsl_to_rgb(h, s, l))

    @classmethod
    def from_hsv(cls, h, s, v) -> Self:
        return cls._from(colorsys.hsv_to_rgb(h, s, v))

    @classmethod
    def from_yiq(cls, y, i, q) -> Self:
        return cls._from(colorsys.yiq_to_rgb(y, i, q))

    def _to(self):
        return (i / 255 for i in self)

    @classmethod
    def _from(cls, rgb):
        return cls(*(min(255, int(265 * c)) for c in rgb))

brightness: float cached property

gamma-weighted average of intensities

rgb: int cached property

Return an integer between 0 and 0xFFFFFF combining the components

closest()

Return the closest named color to self. This is quite slow, particularly in large schemes.

Source code in nc/color.py
39
40
41
42
43
44
def closest(self) -> Self:
    """
    Return the closest named color to `self`.  This is quite slow,
    particularly in large schemes.
    """
    return self.COLORS.closest(self)

distance(other)

Return the distance between this and another color

Source code in nc/color.py
51
52
53
def distance(self, other) -> float:
    """Return the distance between this and another color"""
    return math.sqrt(self.distance2(other))

distance2(other)

Return the square of the distance between this and another color

Source code in nc/color.py
46
47
48
49
def distance2(self, other) -> int:
    """Return the square of the distance between this and another color"""
    d = (i - j for i, j in zip(self, other))
    return sum(i * i for i in d)

Colors

A collection of named colors that acts like an array, a dictionary and a namespace.

Colors is indexed by a type ColorKey, which may be an int, slice, str, Color, orTuple[int, int, int]].

Source code in nc/colors.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class Colors:
    """A collection of named colors that acts like an array, a dictionary
    and a namespace.

    `Colors` is indexed by a type `ColorKey`, which may be an
    `int`, `slice`, `str`, `Color`, or`Tuple[int, int, int]]`.
    """

    def __init__(self, *palettes, canonicalize_gray='gray', default='black'):
        from . import color

        class Color(color.Color):
            COLORS = self

        self.__dict__['Color'] = Color
        self._canonicalize_gray = canonicalize_gray
        self._name_to_rgb = {}
        self._rgb_to_name = {}
        self._palettes = [self._add_palette(s) for s in palettes]

        self._canonical_to_rgb = {
            self._canonical_name(k): v for k, v in self._name_to_rgb.items()
        }
        self._default = self.get(str(default)) or next(iter(self._rgb_to_name))

    def get(self, key: ColorKey, default: Optional[Color] = None):
        """Return the value for `key` if it exists, else `default`."""
        try:
            return self[key]
        except KeyError:
            return default

    def items(self) -> Iterator[Tuple[str, Color]]:
        """Return an iterator of name, Color pairs. Some colors might repeat"""
        return self._colors.items()

    def values(self) -> Iterator[Color]:
        """Return an iterator of Colors. Some colors might repeat"""
        return self._colors.values()

    def keys(self) -> Iterator[str]:
        """Return an iterator of strings, unique color names"""
        return self._colors.keys()

    def closest(self, color: Color) -> Color:
        """
        Return the closest named color to `color`.  This can be quite slow,
        particularly if there are many colors.
        """
        if isinstance(color, list):
            color = tuple(color)
        if color in self._rgb_to_name:
            return color
        return min((c.distance2(color), c) for c in self.values())[1]

    def __call__(self, *args, **kwds):
        return self.Color(*args, **kwds)

    def __getitem__(self, name: ColorKey) -> Color:
        """Try to convert string item into a color"""
        if isinstance(name, (int, slice)):
            return self._color_list[name]

        if isinstance(name, tuple):
            return self.Color(*name)

        canonical = self._canonical_name(name)
        try:
            return self._canonical_to_rgb[canonical]
        except KeyError:
            pass
        raise KeyError(name)

    def __setitem__(self, name, rgb):
        raise KeyError(name)

    def __contains__(self, x: ColorKey) -> bool:
        """Return true if this ColorKey appears in the table canonically"""
        return self._canonical_name(x) in self._canonical_to_rgb

    def __getattr__(self, name: str) -> Color:
        if name.startswith('_'):
            return super().__getattribute__(name)
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name.startswith('_'):
            return super().__setattr__(name, value)
        raise AttributeError(name)

    def __len__(self) -> int:
        return len(self._color_list)

    def __iter__(self) -> Iterator[Color]:
        return iter(self._color_list)

    def __eq__(self, x) -> bool:
        return __class__ == x.__class__ and self._name_to_rgb == x._name_to_rgb

    def __ne__(self, x) -> bool:
        return not (self == x)

    def __add__(self, x):
        cg, d = self._canonicalize_gray, self._default
        c = x if isinstance(x, __class__) else __class__(x)
        palettes = self._palettes + c._palettes
        return __class__(*palettes, canonicalize_gray=cg, default=d)

    def __radd__(self, x):
        other = __class__(
            x, canonicalize_gray=self._canonicalize_gray, default=self._default
        )
        return other + self

    def _add_palette(self, palette):
        if isinstance(palette, str):
            if '.' not in palette:
                palette = '.' + palette
            if palette.startswith('.'):
                palette = 'nc.palette' + palette

            palette = importlib.import_module(palette)

        if not isinstance(palette, dict):
            palette = palette.__dict__

        if 'COLORS' in palette:
            colors = palette['COLORS']
            primary_names = palette.get('PRIMARY_NAMES', ())

        else:
            colors = palette
            palette = {'COLORS': palette}
            primary_names = ()

        colors = {k: self.Color(v) for k, v in colors.items()}
        if not palette.get('PRESERVE_CAPITALIZATION'):
            colors = {k.capitalize(): v for k, v in colors.items()}

        for sub, rep in self._replacements:
            colors = {sub(rep, k): v for k, v in colors.items()}

        self._name_to_rgb.update(colors)

        def best_name(names):
            names.sort(key=lambda n: (len(n), n.lower()))
            pnames = (n for n in names if n in primary_names)
            return next(pnames, names[0])

        names = {}
        for n, c in colors.items():
            names.setdefault(c, []).append(n)

        self._rgb_to_name.update((k, best_name(v)) for k, v in names.items())
        return palette

    def _canonical_name(self, name):
        name = name.lower()
        if self._canonicalize_gray:
            name = name.replace('grey', 'gray')
        return ''.join(i for i in name if i in _ALLOWED)

    @cached_property
    def _colors(self):
        return {k: self.Color(*v) for k, v in self._name_to_rgb.items()}

    @cached_property
    def _color_list(self):
        return list(self._colors.values())

    @cached_property
    def _replacements(self):
        if not (gt := self._canonicalize_gray):
            return ()

        gt = 'gray' if gt is True else gt.lower()
        gf = 'grey' if gt == 'gray' else 'gray'
        if gt not in ('gray', 'grey'):
            raise ValueError('Don\'t understand canonicalize_gray=%s' % gt)

        regular = re.compile(r'\b%s\b' % gf).sub, gt
        upper = re.compile(r'\b%s\b' % gf.capitalize()).sub, gt.capitalize()
        return regular, upper

__contains__(x)

Return true if this ColorKey appears in the table canonically

Source code in nc/colors.py
89
90
91
def __contains__(self, x: ColorKey) -> bool:
    """Return true if this ColorKey appears in the table canonically"""
    return self._canonical_name(x) in self._canonical_to_rgb

__getitem__(name)

Try to convert string item into a color

Source code in nc/colors.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __getitem__(self, name: ColorKey) -> Color:
    """Try to convert string item into a color"""
    if isinstance(name, (int, slice)):
        return self._color_list[name]

    if isinstance(name, tuple):
        return self.Color(*name)

    canonical = self._canonical_name(name)
    try:
        return self._canonical_to_rgb[canonical]
    except KeyError:
        pass
    raise KeyError(name)

closest(color)

Return the closest named color to color. This can be quite slow, particularly if there are many colors.

Source code in nc/colors.py
57
58
59
60
61
62
63
64
65
66
def closest(self, color: Color) -> Color:
    """
    Return the closest named color to `color`.  This can be quite slow,
    particularly if there are many colors.
    """
    if isinstance(color, list):
        color = tuple(color)
    if color in self._rgb_to_name:
        return color
    return min((c.distance2(color), c) for c in self.values())[1]

get(key, default=None)

Return the value for key if it exists, else default.

Source code in nc/colors.py
38
39
40
41
42
43
def get(self, key: ColorKey, default: Optional[Color] = None):
    """Return the value for `key` if it exists, else `default`."""
    try:
        return self[key]
    except KeyError:
        return default

items()

Return an iterator of name, Color pairs. Some colors might repeat

Source code in nc/colors.py
45
46
47
def items(self) -> Iterator[Tuple[str, Color]]:
    """Return an iterator of name, Color pairs. Some colors might repeat"""
    return self._colors.items()

keys()

Return an iterator of strings, unique color names

Source code in nc/colors.py
53
54
55
def keys(self) -> Iterator[str]:
    """Return an iterator of strings, unique color names"""
    return self._colors.keys()

values()

Return an iterator of Colors. Some colors might repeat

Source code in nc/colors.py
49
50
51
def values(self) -> Iterator[Color]:
    """Return an iterator of Colors. Some colors might repeat"""
    return self._colors.values()

About this project