mrkeyoor.com_
Thu 06 Aug 07:41 UTC
PyPICLI & Toolingupdated 06 Aug 2026

termcolor

termcolor is a single pure-Python module that wraps a string in ANSI escape codes so your terminal prints it in colour. The public API is three functions and four constants: colored() returns a coloured string, cprint() prints one and forwards the rest of its arguments to print(), and can_colorize() tells you whether colour is appropriate right now. You pass a colour name like "red", an optional background like "on_cyan", and a list of attributes like ["bold", "underline"]. Since 3.1 you can also pass an (R, G, B) tuple of 0-255 ints for true colour. It has no dependencies, does not track terminal state, and does not know anything about layout, tables, progress bars, or markup. It writes an escape sequence in front of your text and a reset code after it.

Verdict

The right choice when you want a splash of colour in a script and nothing else, and it handles the NO_COLOR conventions correctly so you do not have to. The moment you want a table, a spinner, or nested styles, stop fighting it and install rich.

API stability5/5colored() and cprint() have kept the same positional signature for over a decade. The only removal in 3.0 was the deprecated __ALL__ alias, and 3.1 through 3.3 only added optional features (RGB tuples, public can_colorize, italic).
Docs3/5The README is a complete reference for a library this small: every colour, highlight, attribute, and colour-detection override is documented, plus a per-terminal support table. There is no docs site, no changelog narrative in the README, and no warning about the can_colorize cache or the nesting behaviour, both of which surprise people.
Maintenance4/5Pushed August 2026 with 3 open issues (5 counting PRs) and steady releases through 2025: 3.0 in March, 3.1 in April, 3.2 in October, 3.3 in December. It tracks new CPython releases promptly. The bus factor is the concern: the changelog is almost entirely one maintainer.
Ecosystem4/5Around 24.6M downloads a week, largely as a transitive dependency of build and test tooling, so it is present in most environments already. It has no plugin surface and nothing builds on top of it, so the ecosystem is depth of installs rather than breadth of integration.

Use it if

  • You want one or two lines of colour in a script and refuse to add a dependency tree for it: termcolor has zero runtime dependencies and one module file
  • You need the standard no-colour etiquette handled for you: NO_COLOR, FORCE_COLOR, ANSI_COLORS_DISABLED, TERM=dumb, and a tty check are all built into can_colorize()
  • You are colouring already-formatted strings from elsewhere, such as log lines or subprocess output, where a markup language would fight you over stray brackets
  • You want per-call overrides rather than global state: colored(text, "red", no_color=True) and force_color=True are keyword arguments, not module-level configuration
Skip it if

Setup reality

pip install termcolor and you are done: it is a pure-Python wheel with no runtime dependencies, no compiler, and no post-install step. Version 3.2 dropped Python 3.9, so 3.10 is the current floor and pip will silently resolve you back to an older termcolor on 3.9. The surprises are all behavioural. can_colorize() is decorated with functools.cache, so the very first call freezes the answer for the life of the process: flipping FORCE_COLOR or reassigning sys.stdout afterwards changes nothing unless you call can_colorize.cache_clear(). That same function only ever inspects sys.stdout, so cprint(..., file=sys.stderr) still decides based on stdout, and piping stdout to a file silently kills colour on your stderr diagnostics. Version 3.0 also removed the long-deprecated __ALL__ alias, which breaks the rare code that imported it.

Patterns

Return a coloured stringcolorize-a-string

from termcolor import colored

msg = colored("Deploy failed", "red", attrs=["bold"])
print(msg)
# '\x1b[1m\x1b[31mDeploy failed\x1b[0m'

colored() calls str() on whatever you pass, so numbers and objects work without conversion. The reset code is always appended, even when colour is disabled and the function returns your text unchanged.

Print directly with cprintprint-in-color

from termcolor import cprint

cprint("All 42 checks passed", "green")
cprint("skipped", "yellow", end=" ")
cprint("3 files", "dark_grey")

cprint takes the same first four arguments as colored and forwards every other keyword to print(), so end, sep, and flush all work as usual.

Combine foreground, background, and attributesbackground-and-attributes

from termcolor import colored

banner = colored(
    " CRITICAL ",
    "white",
    "on_red",
    attrs=["bold", "underline"],
)

Attributes are applied in list order and each one prepends its own escape code, so the output has several sequences before the text. Valid attributes are bold, dark, italic, underline, blink, reverse, concealed, and strike; italic arrived in 3.3.

Use 24-bit RGB colourstruecolor-rgb

from termcolor import cprint

cprint("brand colour", (100, 150, 250))
cprint("inverted", (20, 20, 20), (100, 150, 250))

Added in 3.1. Values outside 0-255 or a tuple that is not exactly three long raise ValueError. Terminals without true colour support will show something approximate or nothing, and termcolor does not detect that for you.

Override colour detection for one callforce-or-disable-color

from termcolor import colored

# always plain, e.g. when writing to a log file
plain = colored(text, "red", no_color=True)

# always coloured, e.g. in CI that reports itself as non-tty
forced = colored(text, "red", force_color=True)

These are keyword-only and take precedence over every environment variable. no_color wins over force_color when both are truthy, which matches the ordering documented in the README.

Let the user turn colour offrespect-no-color-env

import os

# Any non-empty value disables colour:
#   NO_COLOR=1 python app.py
#   ANSI_COLORS_DISABLED=1 python app.py
# Any non-empty value forces it:
#   FORCE_COLOR=1 python app.py | tee build.log
print(os.environ.get("NO_COLOR"))

Since 3.0 an empty string no longer counts, so NO_COLOR= behaves as unset. TERM=dumb also disables colour. You get all of this for free; do not reimplement the checks yourself.

Branch on whether colour is usablecheck-tty-first

from termcolor import can_colorize

if can_colorize():
    header = "\N{CHECK MARK} "
else:
    header = "[ok] "

can_colorize became public API in 3.2. Use it when the coloured and plain versions of your output differ in more than escape codes, such as swapping unicode glyphs for ASCII.

Re-detect colour after changing stdout or env varsclear-detection-cache

import os
from termcolor import can_colorize

os.environ["FORCE_COLOR"] = "1"
can_colorize.cache_clear()   # otherwise the first answer sticks
assert can_colorize()

can_colorize is wrapped in functools.cache, so the result of the first call is reused for the whole process. Tests that capture stdout or toggle env vars need cache_clear() between cases or they will assert against a stale answer.

Colour stderr without lying about the terminalcolor-stderr-correctly

import sys
from termcolor import cprint

use_color = sys.stderr.isatty()
cprint("warning: config not found", "yellow",
       file=sys.stderr, force_color=use_color or None)

can_colorize only ever looks at sys.stdout. Without the override, `python app.py > out.txt` on a terminal strips colour from your stderr messages even though stderr is still a tty. Pass None rather than False so the normal detection still applies when stderr is redirected.

Build one coloured span instead of nestingavoid-nesting-resets

from termcolor import colored

# Broken: ' end' comes out uncoloured
bad = colored("start " + colored("inner", "green") + " end", "red")

# Works: colour each piece separately and join
good = (colored("start ", "red")
        + colored("inner", "green")
        + colored(" end", "red"))

colored() appends a single reset at the end, so the inner reset also cancels the outer colour. This is the most common termcolor bug report and the library will not fix it; concatenate coloured spans instead.

Remove escape codes before storing textstrip-ansi-for-logs

import re

ANSI = re.compile(r"\x1b\[[0-9;]*m")

def strip_ansi(text: str) -> str:
    return ANSI.sub("", text)

log_file.write(strip_ansi(colored_line) + "\n")

termcolor ships no strip helper, and len() on a coloured string counts the escape bytes, which quietly breaks any column alignment you compute yourself. Strip before measuring or persisting.

Make escape codes work on old Windows consoleslegacy-windows-console

import sys

if sys.platform == "win32":
    from colorama import just_fix_windows_console
    just_fix_windows_console()

from termcolor import cprint
cprint("now visible in cmd.exe", "cyan")

termcolor never enables virtual terminal processing. Windows Terminal and PowerShell 7 handle ANSI natively, but the classic conout console does not, and users there see raw escape sequences until colorama fixes the console mode.

Alternatives

PackageRegistryPick it when
richPyPIYou need more than colour: tables, panels, progress bars, tracebacks, markdown, and syntax highlighting in one package
coloramaPyPIYou must support legacy Windows consoles or want to wrap sys.stdout once instead of calling a function at every print site
clickPyPIYou are already building the CLI with click: style() and secho() cover the same ground and strip colour when output is not a terminal