La Dispyrition.
Published 28 June 2026 at Yours, Kewbish. 2,575 words. Subscribe via RSS.
Ça paraît trop ardu. By « ça », I mean « rédiger cet article en français, sans la lettre « e » », morceau de phrase dans lequel j’en ai déjà mis une bonne pincée1.
Yet if it was daunting to me, the French writer Georges Perec was happy enough to pick up the slack. He wrote a whole murder-mystery novel without once deigning to use the letter “e”. This type of work is called a lipogram, constraining writers against the use of certain letters. If this’s interesting, I go into similar constraints in Oulipian potential literature in a separate post.
While outlining that other post, I thought about taking a stab at lipograms myself. Admittedly, finagling synonyms around in French or English seemed too abstruse to be fun. But there’s no rule that the language used has to be a spoken one: what if I used a programming language instead, and wrote a program without the letter “e”? In particular, I had a hunch Python’d make it fairly doable, given its builtin and global oddities that’d let me exec() without actually using the letter “e” in the file. This feels like something quine enthusiasts or CTF jail specialists might enjoy — indeed, Oulipian constraints quickly reminded me of pyjails. From a cursory search, it doesn’t seem like anyone’s tried to do this before, but I’d be eager to see other examples.
This post covers two variants of lipogrammatic programming2 in Python, both avoiding “e” and using it as the exclusive vowel. I send many apologies to Perec.
Yzont partis, zux ?
The first lipogrammatic challenge I tackled was e-free programming, in line with Perec’s novel La Disparition (A Void). My only rule is that the letter “e” could not appear in the Python file as it is written to disk, but I could still call methods or use expressions that would evaluate to a name including “e”. In other words, I can’t use exec() or True in my program, but I could call the equivalent method or use conditionals as long as I avoid these specific keywords.
This type of restriction aren’t an entirely new concept. Pyjails, Capture-The-Flag challenges that require you to break out of a restricted Python interpreter, also usually filter methods and permitted characters. For example, you might want to access the text flag file stored on the server alongside the script, but not be allowed to call open() (we aren’t either, since there’s an “e” in open). This writeup features a nice comprehensive list to give you a sense of what typical workarounds are like. However, this guide doesn’t completely fit our needs, since many of these alternatives have “e"s in them — usually challenge authors blanket-ban words instead of letters.
My first vague thought was to replace all the “e"s with format strings and {chr(ord("a") + 5)}s. However, this only works for “e"s in plain strings, not for the “e"s in method calls or other reserved keywords3. In Python, 15/35 = 43% of keywords include an “e”, so this’ll be a major obstacle. For instance, take True or False, two basic literals that necessarily have an “e” in them. Alone, they’re fairly easy to replace: 1==1 also evaluates to True, and 1==0 does the job for False.
However, if we want to translate them in the context of even a basic snippet like:
c = True
if c:
print("Hi")
else:
print("Bye")
We have to be a bit more careful. If c is some static flag, we can replace c = True with c = (1==1), but what do we do with the else branch? We could replace it with an if not c in this case, though this won’t work in general. And obviously, there’s an “e” in “Bye” to remove. We don’t yet have to modify the structure of this program, but you’ll see when we get to the e-only section how this ramps up in difficulty.
Of Python’s reserved keywords, there are 15 that include an “e”4. There are also 2 soft-reserved keywords (case, type) that have an “e”. Here are some highlights of things we can’t do (or in any case, that I haven’t figured out how to circumvent):
defandreturnare off-limits, so functions are limited to basic lambdas- Error handling gets much harder because we can’t use
except elseis prohibited, which forces us to rewrite all conditionals or express them as[else_value, if_value][condition]- Generators are out of the question, since we can’t use
yield
But here are some workarounds for other keywords:
None:"None"is an attribute on__builtins__, which is helpful. Unfortunately, we can’t usegetattrbecause of that pesky “e”, so we’ll have to access it another way. What I came up with is__builtins__.__dict__[dir(__builtins__)[40]].dirsorts the list of attributes on__builtins__, and40is the index of the “None” attribute (so the subexpressiondir(__builtins__)[40] == "None"). Then, we can access the actual value of the key by using__builtins__.__dict__.- I used this trick often, so a handy shortcut I’ve found to get the index of an attribute from its name is
dir(__builtins__).index('fnname'). However, keep in mind the value of__builtins__can be either the module itself or the__dict__attribute, depending on the implementation. This means the indices can shift, and I ran into a few off-by-one issues switching between the interactive Python interpreter and running the file directly. I think this would go away if one justimport builtin‘ed. - Because this rewriting is so verbose, I call it builtin soup.
- I used this trick often, so a handy shortcut I’ve found to get the index of an attribute from its name is
assert: We can do something similar by usingexecvia this builtin soup and manually raising theAssertionError. But of course there are multiple “e"s inAssertionError, so we’ll have to do__builtins__.__dict__[dir(__builtins__)[108]]("rais\x65 Ass\x65rtion\x45rror"), perhaps in a conditionalif not expression_to_be_asserted(thenotreflects the original semantics).while: List comprehensions withmapandfilter(via builtin soup, given the “e”) have been helpful.
There’s an easier way out of all this that takes the whole, original program, escapes it into hex, then exec’s it back. This feels like cheating though, so I try to keep the encoded-exec portions of my programs as small as possible.
The 2e Exert’n
Let’s contrast this with e-only Python before we get into concrete examples. This challenge mirrors Les Revenentes (The Exeter Text), which is an lipogrammatic essay excluding all vowels except “e”. As in the text, we’re still allowed to use the letter “y”, though this only grants us the extra soft-reserved keyword “type”.
To revisit the if-else example, we have nearly the opposite problems this time. We still have to rewrite True, but this time we’re not allowed to use if. This forces us to express the conditional differently:
exec(["pr\x69nt('Bye')", "pr\x69nt('Hey')"][1==1])
We can’t rely on builtin soup this time, given the “u"s and “i"s, which is a pity, since we can finally use getattr. However, we can use exec, which is arguably more powerful. We can call globals like so, and leaving the consonants in the string to be exec‘ed helps with readability.
exec operates in its own context, though. When we need it to impact the larger program state, we can store the results in a mutable list as a container. (Note that we can’t just use a flat variable.) For instance, we could cast a variable with c = [0] to define a container, then exec("c[0] = b\x6f\x6fl(x)") to fill it. With an extra [y, x][r[0]], this can express an or.
There are 30 reserved5 and 2 soft-reserved keywords (case and match) that are forbidden. There’s significant overlap with the first list, since many words use multiple different vowels (i.e. “e” and another vowel, which excludes the keyword from both e-free and e-only variants). Again, some workarounds:
None:exec("ver[0] = N\x6fne")(or just the return value from plainexec(), which should also beNone)assert:exec("r\x61\x69se \x41ssert\x69\x6fnErr\x6fr")— the same idea, just without the builtin soup this time, and a different set of escaped charactersand: use multiplication (whenxandyare booleans,x and yis equivalent tox * y)not: instead ofnot x, we can do the exec container trick above, then take1 - c[0]contains: callgetattr(ebject, "c\x6fnt\x61\x69ns")with: manually call the__enter__and__exec__dunders
In some cases, the language functionality we’re able to use flips. For instance, we can only define methods and not use lambdas in e-only Python, whereas e-free Python was the other way around. One upshot here is that list comprehensions are a less attractive option to replace while loops in e-only Python, and you’ll see in this next section how I struggled with this.
Locked In
I thought a fun idea for a CTF jail challenge would be to force users to submit some lipogrammatic program that would run, solve a problem, then output the flag if the answer matched an oracle. There could be two sister challenges, one with and one without “e"s. I later realized the challenge would be quite unreasonable, though, since access to exec gives users more free reign than usual pyjails, and I suspect there wouldn’t be any solves using the intended constraint.
I still thought it would be an interesting exercise, though, so I took a stab at solving the first half of Advent of Code 2025 Day 1 lipogrammatically. The first day of AoC is usually an easier warmup, and this year’s revolved around a rotating combination lock. The dial started at some known position (50), and the input was a list of instructions to rotate it either left or right. The required output was the number of times the lock’s dial pointed to zero at the end of a rotation.
In normal Python, this is straightforward with some modular arithmetic, but there are still some conditionals and state management that we’ll have to be careful with. Here’s the unconstrained solution I started with:
with open("input.txt", 'r') as x:
lines = x.readlines()
current = 50
count = 0
for line in lines:
offset = int(line[1:])
current = (current + offset if line[0] == "R" else current - offset) % 100
if current == 0:
count += 1
print(count)
First, let’s convert this to e-free Python. With judicious variable naming, we get down from 18 to 5 “e"s:
with open("input.txt", 'r') as x:
listings = x.readlines()
ongoing = 50
count = 0
for listing in listings:
gap = int(listing[1:])
ongoing = (ongoing + gap if listing[0] == "R" else ongoing - gap) % 100
if ongoing == 0:
count += 1
print(count)
We eliminate the remaining “e"s as follows:
open: builtin soup to call the globalreadlines: builtin soup to callgetattr, then an escaped string for the method nameelse: array-based conditional rewriting — note that we must switch the order of the branches again
The final result:
import builtins
with builtins.__dict__[dir(builtins)[137]]("input.txt", 'r') as x:
listings = builtins.__dict__[dir(builtins)[114]](x, "r\x65adlin\x65s")()
ongoing = 50
count = 0
for listing in listings:
gap = int(listing[1:])
ongoing = [ongoing - gap, ongoing + gap][listing[0] == "R"] % 100
if ongoing == 0:
count += 1
print(count)
Next, we can look at the e-only variant. After some comedic variable renaming (modelled after the artistic liberties of the Monk Exeter Text translation), we have 11 vowels to worry about:
with open("npt.txt", 'r') as x:
entrees = x.readlines()
present = 50
nember = 0
for entrees in entrees:
defference = int(entrees[1:])
present = (present + defference if entrees[0] == "R" else present - defference) % 100
if present == 0:
nember += 1
print(nember)
Here’s how I got rid of them:
open:execand an escaped stringreadlines: idem.forloop: wrapped this into a function, then usedexecto do list comprehension with an escapedlistandmap— note that thepresentandnembervariables are now lists in order for the function to mutate them as global stateint: same asopen- inline
presentupdate conditional: same array-based conditional rewriting as with e-less Python nemberincrement conditional: another array-based rewriting, this time varying the value being added tonember[0]print: same asopen
The final program:
entrees = []
exec("entrees = \x6fpen('npt.txt', 'r').re\x61dl\x69nes()")
present = [50]
nember = [0]
def step(entree):
defference = [0]
exec("defference[0] = \x69nt(entree[1:])")
present[0] = [present[0] - defference[0], present[0] + defference[0]][entree[0] == "R"] % 100
nember[0] += [0, 1][present[0] == 0]
exec("l\x69st(m\x61p(step, entrees))")
exec("pr\x69nt(nember[0])")
I tried to limit my use of the exec-escaped-strings combo to just the global functions. I also tried to use execs to wrap only single lines instead of running whole blocks of code, which made the global lists necessary to capture state. These still feel a little not-in-spirit, though, so I’m still looking if I can figure out an alternative.
Both these variants were quite complicated to translate, even for such a simple original program. The conditional handling especially was quite tricky, and I’m not sure how I would handle larger conditionals with multiple variable updates in the indented block in general. Maybe I’d use one array-rewritten line per variable update?
Conclusion
Lipogrammatic programming is quite fun at first. I still think it’d be a great CTF challenge if sandboxed appropriately. Like with standard pyjails, there are patterns that repeatedly come up (e.g. builtin soup), and over time one might develop a more exhaustive list of workarounds, since this post only covers some basic keyword manipulation. It might even be feasible to write a general compiler from a generic, unconstrained Python program to an e-free/e-less variant, with enough AST manipulation. I’ll admit that I’m not keen on trying much more of this constraint manually, though — it gets tedious looking up builtin offsets and translating even simple toy programs.
I think lipogrammatic programming could work quite well in other dynamic languages. For example, I’ve seen a (rare) few Lua jail challenges before, so the same object manipulation should be possible. JavaScript seems like another ideal candidate, with even quirkier evaluation semantics. In a compiled language, I’m also sure one could rig up a preprocessing pass to convert special characters back into the appropriate vowels that would make the process much easier. But again, I suspect this hits too close to the exec-large-encoded-string cheating to engage with the spirit of the constraint.6
This was a fun afternoon of exploring Python docs and hacking away. For those who regularly do AoC in niche, esoteric languages, perhaps lipogrammatic Python could be a cute next challenge. If you end up trying it, or find an existing (intentional) example in the wild, please feel free to share! You’ll find my (sadly non-lipogrammatic) email on my main site.
-
Translation: It seems too arduous. By « it », I mean « writing this article in French, without the letter « e » », which is a phrase in which I’ve already sprinkled a good pinch of them. (The first bit was as far as I got trying to intentionally write any lipograms.) ↩︎
-
“La liprogrammation”, c’est un portmanteau juste ? ↩︎
-
And after some more poking, I realized that I could just use
\xhex codes, which are a bit more compact. ↩︎ -
['False', 'None', 'True', 'assert', 'break', 'continue', 'def', 'del', 'elif', 'else', 'except', 'raise', 'return', 'while', 'yield']↩︎ -
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'elif', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'while', 'with', 'yield']↩︎ -
On the spoken language side of things, I came across this recent paper on using LLMs to convert existing texts into lipogrammatic variants. A neat exploration. ↩︎