Python: introducing emojet, a fast emoji lookup library

What do you know, my old archive images don’t feature pictures of jet planes, just prop ones.

New package just landed!

emojet is an emoji library for Python: it converts between emoji and their names, in both directions, plus the searching and lookup functions that go with that. It covers the core API of the emoji package, a library that been available for this job since 2014, using the same names and the same data. The difference is that emojet does the work in Rust, running 3.5x faster for conversion, 70 times faster for deconversion, and using about 40% less memory.

Use emojize() to convert names to emoji:

>>> import emojet
>>> emojet.emojize("Python is fun :thumbs_up:")
'Python is fun 👍'

…and demojize() to deconvert emoji to their names:

>>> emojet.demojize("Python is fun 👍")
'Python is fun :thumbs_up:'

Names come in 14 languages, plus the English aliases that GitHub and Slack use:

>>> emojet.emojize("Python ist toll :daumen_hoch:", language="de")
'Python ist toll 👍'
>>> emojet.emojize("Python is fun :thumbsup:", language="alias")
'Python is fun 👍'

See the documentation for the full API.

The motivation

Part of my work for my client Rippling is speeding up their project’s startup time. I use several techniques to achieve this, as covered in my previous post on optimizing startup time. I’ve spent a lot of time finding slow-to-import third-party packages and choosing what to do with them, like deferring their imports, or upgrading to a faster version.

The emoji package stood out from profiling data: it took 22ms to import while providing minimal functionality. 22ms is not the slowest package by far, but it is still noticeable on a trace. Further profiling revealed that most of its import time was spent loading and parsing a 520 KB JSON file containing the emoji characters and names.

Initially, I deferred the emoji imports, like:

-from emoji import emojize

 def show_message(text):
+    from emoji import emojize
     print(emojize(text))

This pushes the 22ms cost out of most processes, at the cost of loading the package later, after initialization is complete. But the idea of a faster rewrite smouldered away at the back of my mind, based on these motivations:

  1. The data rarely changes (on Unicode annual updates), perfect for baking into a binary format.
  2. The API is fairly simple.
  3. String manipulation is often much faster in a lower-level language.

Anyway, I have been trying out Claude more recently, thanks to a free subscription that Anthropic gifted me due to my open source contributions. I decided to fire Claude at the task and whaddaya know, bot did good, and after a bunch of iteration and minor touchups, we have emojet.

Benchmarkation

The emojet repo bundles a benchmark script for comparison with the original emoji package. Here are some of the results comparing emojet 1.0.0 against emoji 2.15.0, on Python 3.14 (macOS, ARM):

BenchmarkemojiemojetSpeedup
import20.57 ms869.94 µs23.7x
import + first demojize()24.45 ms911.13 µs26.8x
demojize()345.12 µs4.94 µs69.9x
emojize()29.23 µs8.33 µs3.5x

Memory use is also lower: importing and calling demojize() once uses about 17.4 MB of memory with emojet versus 29.2 MB with the emoji package.

You can check these numbers yourself. The benchmark script uses PEP 723 inline script metadata, so uv will install the latest releases of both packages into a temporary environment for you:

uv run scripts/benchmark.py

How it works

emojet is a Rust extension module, built with PyO3 and maturin, with all of the emoji data compiled into static tables:

Nothing is parsed at import time. The tables sit in the read-only data section of the extension module, so the operating system pages them in on demand, and they’re shared between processes. That’s where the import-time win comes from: the remaining ~900 µs is mostly the dynamic loader doing its job.

The scanners work on UTF-8 bytes end to end, writing each result into a single buffer rather than joining a list of pieces. They also skip runs of ASCII text with a byte-per-character test, since almost no emoji starts with an ASCII character—and most text is mostly ASCII. That’s why demojize() on a mixed string sees a bigger speedup than emojize(), which has to hunt for delimiters.

The data itself is generated by scripts/generate_data.py, which follows the emoji package’s own pipeline: Unicode’s emoji-test.txt and emoji-variation-sequences.txt, CLDR annotations for translated names, and GitHub’s gemoji database for aliases. The generated file is checked in, so installing or building emojet downloads nothing. 1.0.0 ships Unicode 17.0.0 data, matching emoji 2.15.0.

Fin

If your project has emoji conversion needs, please give emojet a try and let me know how it goes.

One emojo at a time,

—Adam


Read my book Boost Your Git DX to Git better.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: