# pygments.rb keeps one Python per Ruby process, and the pipe is the whole design

> pygments.rb is a Ruby wrapper around the Pygments syntax highlighter that talks to a long-lived Python child process over a pipe, replacing the older embedded interpreter approach and the github/albino project. It gives you the full Pygments formatter and style catalogue from Ruby.

**pygments/pygments.rb** — 💎 Ruby wrapper for Pygments syntax highlighter

- Repository: https://github.com/pygments/pygments.rb
- Stars: 583 · Forks: 141
- Language: Ruby
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pygments-pygments-rb

## A pipe to Python, not an embedded interpreter

pygments.rb is a Ruby wrapper for the Pygments syntax highlighter, and the README states the mechanism in one sentence: it works by talking over a simple pipe to a long-lived Python child process.

That single design choice separates it from the two things it replaced. The README says the library replaces github/albino, as well as an older version of pygments.rb that used an embedded Python interpreter. So the project has been rewritten twice in its life to get away from running Python inside the Ruby process, and this time the answer was a separate child process talking over a pipe.

The reason a pipe and not a socket or a subprocess per call is worth noting: the child stays alive, so the Pygments import and lexer setup cost is paid once rather than on every request.

The process model is per Ruby process, and the README is explicit about it. Each Ruby process that runs has its own personal Python, and the example given is that 4 Unicorn workers will have one Python process each. So the memory cost scales with the worker count, not with the request count. If a Python process dies, a new one is spawned on the next pygments.rb request, which means a crash is a latency spike rather than an outage.

The system requirements are Python 3.8 or newer and Ruby 2.7 or newer. Both are floors, so the project runs on a wide range of runtimes, at the cost of assuming a Python is present on the machine at all.

## Installing it as a gem, or into a Gemfile

Installation is the ordinary Ruby way, with a line for the Gemfile and a line for a global install.

```ruby
gem 'pygments.rb'
```

Then you install the dependencies.

```shell
$ bundle install
```

Or you install the gem globally, without Bundler.

```shell
$ gem install pygments.rb
```

The gem is published on RubyGems under the name pygments.rb, and the README's own header points at the gem page and at the PyPI-independent Pygments project it wraps.

The dependency is implicit and easy to miss. The gem installs the Ruby side, but the thing doing the actual highlighting is Pygments, which is a Python package. The README does not give a pip install line in the installation section, so if you are provisioning a container or a CI image, the Python side has to be installed separately, and the version of Pygments you install is what determines which lexers and styles are available to the Ruby API.

That is a real operational consideration for a gem whose whole purpose is to bridge two runtimes. A bundle install does not give you a working highlighter on a machine without Python and Pygments.

## Highlighting, and the options hash for everything else

The usage section starts with requiring the module, and the module name is pygments even though the gem is pygments.rb.

```ruby
require 'pygments'
```

Highlighting a file is a single call with a lexer name.

```ruby
Pygments.highlight(File.read(__FILE__), lexer: 'ruby')
```

The lexer argument is a Pygments lexer name, so the set of languages you can highlight is the set Pygments knows, not a fixed list in this gem.

Anything else goes through the options hash. Encoding and other lexer or formatter options are passed that way, for example with an encoding option set to utf-8.

```ruby
Pygments.highlight('code', options: {encoding: 'utf-8'})
```

The formatter is also an option, and pygments.rb uses the HTML formatter by default. You can select a different one by name, and the README shows bbcode and terminal as examples, which is the sign of a wrapper that exposes the underlying library rather than a curated subset of it.

```ruby
Pygments.highlight('code', formatter: 'bbcode')
Pygments.highlight('code', formatter: 'terminal')
```

For HTML output you also need the stylesheet, and there is a dedicated method for that.

```ruby
Pygments.css
Pygments.css('.highlight')
```

The first form gives you the CSS for the whole document, the second scopes it to a selector, and a style option selects a specific Pygments style, for example monokai. The style is chosen at the same time as the CSS is generated, which is the detail to remember: the style is a property of the stylesheet, not of a single highlighted fragment, so one document has one palette.

## Discovering lexers, and pointing at your own Pygments install

The other high level API methods return arrays describing what is available, and the README names three of them.

```ruby
Pygments.lexers
Pygments.formatters
Pygments.styles
```

This is more useful than it first appears. Because the available lexers and formatters depend on which Pygments version is installed on the machine, these three calls are the way an application discovers what it can actually do, rather than hardcoding a list that will drift from the installed Pygments.

There is also a way to control which Pygments is used, by passing a path to Pygments.start.

```ruby
Pygments.start("/path/to/pygments")
```

That matters in practice. If the machine has a system Pygments and a vendored one, or if the Pygments version is pinned by your deployment rather than by the gem, this is the hook that decides which one is used.

The repository layout backs up the Python dependency story. There is a vendor/ directory at the top level, alongside lib/, spec/, the gemspec, a Gemfile, a Rakefile, a .rubocop.yml and a CHANGELOG.adoc. A vendor directory in a gem usually means the Python side is being carried along with the Ruby gem, which is the most likely answer to how the Python dependency is satisfied in practice, even though the README does not spell it out.

The README also names the three authors, Ted Nyman, Aman Gupta and Marat Radchenko, which is the kind of attribution that tells you this is a project with a specific history rather than a fresh port.

## Timeouts and logging, the two knobs that keep a pipe from hurting you

Two operational controls are documented, and both are about the failure mode that a long-lived child process introduces.

The first is logging. If you want logging, you set the environmental variable MENTOS_LOG to a file path for your logfile. The name is not explained in the README, and neither is the format or the log levels, so treat it as a switch rather than a documented feature.

The second is the timeout, and it is the one that matters. You can apply a timeout to pygments.rb calls by specifying a number of seconds in the MENTOS_TIMEOUT environmental variable, or by passing the timeout argument, and the argument takes precedence over the environment variable.

```ruby
Pygments.highlight('code', timeout: 4)
```

The reason to care is structural. A pipe to a child process can block. If the Python side hangs on a pathological input, a request in your Ruby process waits with it, and in a web application that is a stuck worker rather than a slow response. A timeout turns that into a failure you can handle.

The precedence rule is worth remembering in deployment. Setting MENTOS_TIMEOUT gives you a global default across the process, and the argument lets an individual call override it, which is the shape you want when one call site is known to be expensive and the rest are not.

The same environment variable mechanism, and the fact that both are named with the same prefix, suggests a small family of settings rather than a single feature.

## The benchmark shows what the long-lived process actually buys

The README includes a benchmark section with two runs of a script called bench.rb, and the numbers are worth reading for what they reveal about the design.

```text
   pygments popen                                0.010000   0.010000   0.020000 (  0.460370)
   pygments popen (process already started)      0.010000   0.000000   0.010000 (  0.272975)
   pygments popen (process already started 2)    0.000000   0.000000   0.000000 (  0.273589)
```

Each run reports a size and an iteration count, and then three rows for the same operation. The first row is the cold case, where the Python process has to be started. The second and third are the warm cases, where the process is already running.

The pattern is the whole argument for keeping the child alive. The cold real time of 0.46 seconds drops to 0.27 seconds once the process is already started, and the second warm run costs the same again. Process startup is roughly a third of the total time in the small case, and it is the part you avoid by not paying it per request.

The larger run, with 15523 bytes over 10 iterations, shows the same shape with a cold real time of 0.82 seconds against 0.68 seconds warm. The gap narrows in relative terms as the payload grows, which is what you would expect if the fixed cost is process startup.

These are the project's own numbers on its own hardware, and the user, system and total columns are coarse, so treat them as a shape rather than a prediction. What they do establish is that the pipe design is not incidental, and that the difference between a warm and a cold child is measurable in the tens of milliseconds.

## Where pygments.rb is the wrong tool

Three cases stand out.

The first is when you cannot run Python. The gem is a wrapper, not a port, and the README's system requirements start with Python 3.8 or newer. A minimal container image, a Lambda style environment, or a platform that ships only Ruby has to add a Python runtime and Pygments, and then it is carrying two language runtimes for a job that some of them do in Ruby.

The second is when you need highlighting in a hot path with a lot of concurrent requests. The process model is one Python per Ruby process, so a multi worker server has multiple Python processes, and each request is a round trip over a pipe. The benchmark shows a warm call at roughly 0.27 seconds of real time in the small case, which is not a figure you would want on a page that renders code on every request. A static highlighting step at build time is a better fit for that case.

The third is when you want a specific, small, fixed set of languages with no surprises. The lexers, formatters and styles are all resolved from the installed Pygments, so what you get depends on a Python package version that the Gemfile does not control. If you need to know that a specific language always highlights the same way, that is a dependency you have to pin on both sides, and the README does not describe how the Python version is pinned.

For everything else, the argument is simple. Pygments has a very large lexer catalogue and a good set of styles, and this library exposes all of it from Ruby for the cost of one pipe.

## MIT licensed, with major versions arriving quickly

The licence is MIT, with the README granting free use under those terms and pointing at the LICENSE file for the full text. The copyright line names Ted Nyman, Aman Gupta and Marat Radchenko, 2012 to 2021, which is a long working life for a small wrapper.

The release history is the part to look at carefully. The recent releases are v5.0.0 on 2026-07-22, v4.0.0 on 2025-06-25 and v3.0.0 on 2024-05-14. Three major versions in about two years, and no patch releases listed among them.

A major bump in a library with a small API surface usually means something changed in the contract rather than the internals, and the CHANGELOG.adoc file in the repository is where that is recorded. It is worth reading between versions rather than assuming a minor bump is safe, because the sequence here suggests the maintainers are still settling the API.

On maintenance, the repository is not archived and the last push was on 2026-10-14, so work is landing on master. The development instructions are minimal and conventional: after checking out the repo, run bundle install to install dependencies, then bundle exec rake spec to run the tests. The presence of a .rubocop.yml and a Rakefile means the project uses the standard Ruby lint and test tooling rather than anything unusual.

The irony worth noting is that the repository originally existed to replace github/albino, and it has now had two major rewrites of its own. The pipe based design looks like the arrangement that will last.

## Conclusion

Adopt pygments.rb if you render syntax highlighting from Ruby and you want the whole Pygments lexer, formatter and style catalogue rather than a hand rolled subset. Do not adopt it if your application cannot run a Python child process, or if you only need to highlight a small fixed set of languages. Before you depend on it, check that the Python on the target machine satisfies the Python 3.8 or newer requirement, set MENTOS_TIMEOUT so a hung Pygments process cannot stall a request, and read the CHANGELOG.adoc between major versions, since releases go 3.0.0, 4.0.0 and 5.0.0 in fairly quick succession.

## FAQ

### How does pygments.rb work?

It talks over a simple pipe to a long-lived Python child process. Each Ruby process has its own personal Python, so 4 Unicorn workers will have one Python process each, and if a Python process dies a new one is spawned on the next pygments.rb request.

### What are the system requirements for pygments.rb?

Python 3.8 or newer and Ruby 2.7 or newer. The gem itself is Ruby, but Pygments is a Python package, so a Python runtime with Pygments has to be present on the machine.

### How do I set a timeout in pygments.rb?

Set the number of seconds in the MENTOS_TIMEOUT environmental variable, or pass the timeout argument, which takes precedence over the environment variable. Logging is enabled by setting the MENTOS_LOG environmental variable to a file path.

### Which formatters and styles can I use in pygments.rb?

The HTML formatter is the default and others are selected by name, with bbcode and terminal given as examples. Pygments.css generates the stylesheet, optionally scoped to a selector, and a style option such as monokai picks the palette. Pygments.lexers, Pygments.formatters and Pygments.styles return the available choices.

### Can I use a different Pygments installation with pygments.rb?

Yes. You specify the path to your own Pygments installation with Pygments.start, for example Pygments.start("/path/to/pygments").

## Sources

- [Issues](https://github.com/pygments/pygments.rb/issues)
- [License: MIT](https://github.com/pygments/pygments.rb/blob/master/LICENSE)
- [pygments/pygments.rb on GitHub](https://github.com/pygments/pygments.rb)
- [README](https://github.com/pygments/pygments.rb/blob/master/README.md)
- [Releases](https://github.com/pygments/pygments.rb/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pygments-pygments-rb
