mrkeyoor.com_
Wed 04 Nov 01:51 UTC
PyPIUtilsupdated 01 Nov 2026

pygments review

Pygments 2.21.0 is a Python syntax highlighter that labels source text with lexical token types, then gives those tokens to formatters for HTML, terminal color, LaTeX, SVG, images, and other outputs. Its README says it handles more than 500 languages and text formats. It does not build an AST or decide whether code is valid. The 2.21.0 release adds BitBake, Caddyfile, CEL, and PureScript lexers plus the Night Owl style. It also supports Python 3.15, fixes catastrophic backtracking in XML detection, and leaves quote characters literal in HTML formatter output. In our sandbox, `import pygments` worked in 0.01 seconds; the distribution is pure Python and has no `py.typed` marker.

Verdict

Our Pygments 2.21.0 install finished in 0.3 seconds, occupied 6 MB, imported in 0.01 seconds, and showed 0 known vulnerabilities, so broad server-side highlighting costs little to add. Use it for lexical color across many formats; choose a parser for structural work, and isolate highlighting when strangers supply the source.

We installed it

Lab card: what happened when we installed pygmentsScreenshot of pygments documentation
Install✓ · 0.3s1 package on disk · 6 MB
Import✓import pygments in 0.01s · pure Python · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does pygments install cleanly?

Yes. In a fresh container with an empty cache, pip install pygments finished in 0.3s, leaving 1 package and 6 MB on disk. pip-audit reported no known vulnerabilities.

What does pygments need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import pygments succeeded in 0.01s.

pygments or tree-sitter: which should you use?

tree-sitter: Choose tree-sitter 0.26.0 when you need incremental parse trees, named nodes, and structural queries instead of ready-made HTML formatters. Our Pygments 2.21.0 install finished in 0.3 seconds, occupied 6 MB, imported in 0.01 seconds, and showed 0 known vulnerabilities, so broad server-side highlighting costs little to add.

When should you not use pygments?

You need syntax trees, symbol resolution, structural queries, validation, or refactoring. Pygments emits lexical token types and none of those parser results

API stability5/5Pygments 2.21.0 still uses the long-standing `highlight()`, `lex()`, lexer lookup, formatter, filter, token, style, and entry-point interfaces that define the 2.x series. This release adds four lexers and one style without replacing that workflow. Rendered output is less stable than the calling API: fixes for C#, Markdown, Kotlin, Ruby, TypeScript, XML, and other lexers can change token spans, while `HtmlFormatter` now writes quote characters literally. Pin the package when generated markup is checked into a repository or compared as a golden file.
Docs4/5The official documentation has separate references for the Python API, command line, every bundled lexer and formatter, token classes, filters, styles, plugins, Unicode handling, and custom lexer development. Option descriptions spell out details such as `hl_lines` using input-relative numbers and `noclasses=True` increasing HTML size. The security page also gives concrete process-timeout and concurrency advice. Finding the whole production picture takes several pages, especially when you need lexer selection, HTML CSS, encodings, and hostile-input handling together.
Maintenance4/5Version 2.21.0 was released on August 17, 2026, and GitHub records the latest repository push on that date. The repository is not archived and currently reports 665 open issues and pull requests. The release adds four lexers, updates dozens more, fixes XML catastrophic backtracking, and repairs HTML, LaTeX, raw-token, and Groff formatter behavior. That is active maintenance across a very wide grammar set, although the open queue is large enough that users of niche languages should read lexer-specific reports before upgrading.
Ecosystem5/5PyPI download data counted 205,449,688 Pygments downloads in the latest week, and GitHub reports 2,208 stars. The project README claims support for more than 500 languages and text formats. Its formatter set covers web pages, terminals, documents, and images, while installed packages can register extra lexers, formatters, filters, and styles through entry points. Those plugins are visible to normal lookup calls and the `pygmentize` command, which is why the same extension can also appear inside tools such as Sphinx and MkDocs.

Discussed on

  1. hnGitHub dropped Pygments184 points
  2. hnPygments, a generic syntax highlighter85 points
  3. hnPocoo (Flask, Jinja 2, Pygments, Sphinx, Werkzeug)32 points
  4. hnAsk HN: check out my micro app - pygments syntax highlighter via HTTP25 points
  5. hnPygments, a Generic Syntax Highlighter14 points

Use it if

  • A Python documentation site or code host must highlight many languages on the server through one API
  • The same token stream needs to become classed HTML, ANSI terminal text, LaTeX, SVG, or an image
  • Source pages need formatter controls such as numbered lines, linkable line IDs, selected-line emphasis, and swappable color styles
  • Your team has a private DSL and wants its lexer, formatter, filter, or style discoverable by Pygments, Sphinx, MkDocs, and `pygmentize` through package entry points
Skip it if

Setup reality

We installed Pygments 2.21.0 in a fresh, unprivileged Python 3.12 Bookworm sandbox with 3 CPUs, 8 GB of RAM, and no cache. Installation finished in 0.3 seconds and left one package using 6 MB. import pygments worked in 0.01 seconds, while pip-audit reported 0 known vulnerabilities. The sandbox report counted one direct dependency, found pure Python code, and confirmed Python 3.9 or newer. It found no py.typed marker. Its license scan returned unknown; current PyPI metadata declares BSD-2-Clause.

No account, token, background service, or required config file stands between installation and first output. HTML needs one extra piece: HtmlFormatter returns a classed fragment by default, without its stylesheet. Generate CSS with get_style_defs() using the same formatter settings as the markup. If you choose Night Owl, new in 2.21.0, set that style on both paths. noclasses=True avoids a separate stylesheet by repeating inline declarations, which increases output size on long listings.

Language selection needs a policy. get_lexer_by_name() is predictable when the caller supplies an alias; filename lookup can inspect the source to break ties. Unknown aliases or filenames raise ClassNotFound. The CLI docs call content-only guessing unreliable, so a user-facing service should fall back to TextLexer instead of silently choosing a doubtful grammar. Lexer defaults also remove leading and trailing newlines and add a final newline. Set stripnl=False or ensurenl=False when exact whitespace matters.

Treat untrusted source as a workload limit problem. The project says a malformed or adversarial input can keep a matcher running indefinitely or consume substantial memory. Version 2.21.0 fixes one XML backtracking case, but the README still recommends a short process timeout and a cap on concurrent workers. Installed plugins run as Python code in the host process. Windows terminal coloring requires the windows-terminal extra; redirected output should avoid ANSI formatters.

Patterns

Render source as classed HTML render-html-fragment

from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_lexer_by_name

lexer = get_lexer_by_name("python", stripnl=False)
formatter = HtmlFormatter(cssclass="source", wrapcode=True)
fragment = highlight(code, lexer, formatter)

`HtmlFormatter` returns a fragment with token class names by default. `wrapcode=True` adds a `<code>` element, but the page still needs matching CSS.

Build the stylesheet for Night Owl export-theme-css

from pathlib import Path
from pygments.formatters.html import HtmlFormatter

formatter = HtmlFormatter(style="night-owl", cssclass="source")
css = formatter.get_style_defs(".source")
Path("syntax.css").write_text(css, encoding="utf-8")

Pygments 2.21.0 adds the `night-owl` style. The selector passed to `get_style_defs()` must match the wrapper used for rendered fragments.

Handle an unknown language explicitly resolve-language-alias

from pygments.lexers import TextLexer, get_lexer_by_name
from pygments.util import ClassNotFound

try:
    lexer = get_lexer_by_name(language, ensurenl=False)
except ClassNotFound:
    lexer = TextLexer(ensurenl=False)

`get_lexer_by_name()` raises `ClassNotFound` for an unsupported alias. A `TextLexer` fallback keeps the input readable without claiming a language match.

Use both filename and content for lexer choice infer-lexer-from-file

from pygments.lexers import TextLexer, get_lexer_for_filename
from pygments.util import ClassNotFound

try:
    lexer = get_lexer_for_filename(filename, code, stripnl=False)
except ClassNotFound:
    lexer = TextLexer(stripnl=False)

`get_lexer_for_filename()` uses source text to rank multiple filename matches. A filename with no matching lexer still raises `ClassNotFound`.

Number, link, and emphasize lines add-line-navigation

from pygments.formatters.html import HtmlFormatter

formatter = HtmlFormatter(
    linenos="inline",
    linenostart=80,
    linespans="src",
    hl_lines=[2, 4],
)

`hl_lines` counts from 1 inside the supplied snippet, even when `linenostart` displays 80. `linespans="src"` creates IDs such as `src-80`.

Return a self-styled HTML fragment embed-inline-styles

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import JsonLexer

fragment = highlight(
    payload,
    JsonLexer(),
    HtmlFormatter(style="default", noclasses=True),
)

`noclasses=True` puts declarations on each generated span. It removes the stylesheet step at the cost of larger repeated markup.

Color output only for an interactive terminal color-terminal-output

import sys

from pygments import highlight
from pygments.formatters import Terminal256Formatter
from pygments.lexers import PythonLexer

rendered = (
    highlight(code, PythonLexer(), Terminal256Formatter())
    if sys.stdout.isatty()
    else code
)
sys.stdout.write(rendered)

`Terminal256Formatter` emits ANSI escape sequences. Checking `isatty()` keeps redirected files and pipelines free of color control bytes.

Read comments from the token stream inspect-comment-tokens

from pygments import lex
from pygments.lexers import PythonLexer
from pygments.token import Comment

comments = [
    text
    for token_type, text in lex(code, PythonLexer())
    if token_type in Comment
]

Pygments token membership includes child types such as `Comment.Single`. The result carries lexical labels, with no scope or symbol information.

Retag TODO and FIXME inside comments mark-code-tags

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import PythonLexer

lexer = PythonLexer()
lexer.add_filter("codetagify", codetags=["TODO", "FIXME"])
html = highlight(code, lexer, HtmlFormatter())

The `codetagify` filter changes matching words only inside comments and docstrings. Formatters receive those matches as `Comment.Special` tokens.

Create a complete HTML file write-standalone-html

from pathlib import Path

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import PythonLexer

page = highlight(
    code,
    PythonLexer(),
    HtmlFormatter(full=True, title="worker.py", style="night-owl"),
)
Path("worker.html").write_text(page, encoding="utf-8")

`full=True` produces a complete document and includes style rules. Fragment mode is the better fit when an existing layout owns the page shell.

Generate an HTML page with pygmentize highlight-from-cli

pygmentize -l python -f html \
  -O full,style=night-owl,linenos=inline \
  -o worker.html worker.py

`-l` and `-f` bypass inference, while `-o` prevents terminal formatting from becoming the default. The `-O` value is comma-separated.

Expose a lexer through package metadata register-custom-lexer

[project.entry-points."pygments.lexers"]
acme-log = "acme_lexer:AcmeLogLexer"

Pygments discovers custom lexers from the `pygments.lexers` entry-point group in installed distributions. Entry-point results are cached inside a running process, so install plugins before workers start.

Alternatives

PackageRegistryPick it when
tree-sitterPyPIChoose tree-sitter 0.26.0 when you need incremental parse trees, named nodes, and structural queries instead of ready-made HTML formatters.
richPyPIChoose Rich 15.0.0 when the deliverable is a terminal UI, traceback, or syntax panel; Rich depends on Pygments for its code coloring.
highlight.jsnpmChoose highlight.js 11.12.0 when highlighting belongs in a browser or Node process and language autodetection is useful.
prismjsnpmChoose Prism 1.30.0 for a browser site that loads an explicit set of language components and controls its own theme assets.

More utils guides

lru-cache · type-fest · ajv · zod · p-limit · find-up · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.