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.
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.
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
- You are building any real terminal UI: there are no tables, panels, trees, progress bars, syntax highlighting, or word wrapping here, and rich gives you all of it with colour included
- You want colour in your existing print statements without rewriting them: colorama and rich both offer drop-in patching, termcolor makes you call colored() at every site
- You nest colours: colored() appends a reset code at the end of every string, so wrapping an already coloured string means the outer colour stops at the inner reset and the rest of the line comes out plain
- You target old Windows consoles: termcolor emits raw escape codes and never enables virtual terminal processing, so you still need colorama on anything before Windows 10's modern console
- A typo in a colour name is a KeyError at runtime, not a validation error, because colour lookup is a plain dict index inside colored()
- The project is small in every sense: 325 stars, three open issues, one main maintainer, and a scope that has not grown in years, which is fine for a stable utility but leaves nowhere to go when you need more
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
| Package | Registry | Pick it when |
|---|---|---|
| rich | PyPI | You need more than colour: tables, panels, progress bars, tracebacks, markdown, and syntax highlighting in one package |
| colorama | PyPI | You must support legacy Windows consoles or want to wrap sys.stdout once instead of calling a function at every print site |
| click | PyPI | You are already building the CLI with click: style() and secho() cover the same ground and strip colour when output is not a terminal |