EPISODE 05

Two Pythons

The Schism · 2008–2020

Python 3.0 ships and almost nobody comes. Twelve years of schism over strings, and the trauma that still governs how the language changes.

Episode 05 artwork — Two Pythons

December 3, 2008.

Python 3.0 ships. It is, by most benchmarks, slower than 2.6. Almost nobody upgrades.

The World

Consider the moment. It is December 2008, and the global financial system has just come apart. Budgets are frozen everywhere. If you walk into your manager’s office and propose spending a quarter rewriting code that already works — not to add a feature, not to fix a bug, just to keep up with a language release — you will be laughed out of the room. Porting working software is precisely the kind of expense a crisis deletes first.

Then, before the crisis even clears, the mobile boom arrives. Suddenly every company is racing to build. New apps, new backends, new APIs, all on deadline. Migration is the opposite of momentum: it consumes engineers and produces, if you do it perfectly, software that behaves exactly the way it did before. Everyone is building. Nobody is migrating.

So the timing could hardly have been worse. And yet the break was becoming unavoidable, because the thing Python 3 existed to fix was no longer a corner case. The web had gone global. Text was arriving in Cyrillic, in Arabic, in Han characters; emoji were coming. And in every Python 2 application there was a landmine with a name — UnicodeDecodeError — buried under the happy path, waiting for real-world traffic to step on it.

The Pressure

To understand the schism, you have to understand the sin, and the sin is about what a string actually is.

A computer does not store text. It stores bytes — raw numbers, 0 to 255. To turn bytes into text you need an encoding, a codebook that says which numbers mean which characters. Get the codebook wrong and “café” comes back as garbage — the phenomenon has a name, mojibake, and anyone who used the early global web has seen it. Text and bytes are therefore different things: one is meaning, the other is storage, and a well-behaved language should never confuse them.

Python 2 confused them by design. Its str type was bytes. Actual text lived in a separate unicode type, and — here is the trap — the language would silently, implicitly convert between the two whenever they met. Implicit coercion feels helpful, and that is exactly what makes it dangerous. As long as your data is plain ASCII — English letters, digits, the characters where bytes and text happen to agree — the silent conversion always succeeds. Your code works on your machine. It works in the demo. It passes the tests, because the tests use ASCII too. Then a user in another country types their own name, the coercion hits a byte with no ASCII meaning, and the program dies in production at 2 a.m. with a UnicodeDecodeError nowhere near the line that caused it. The bug was not in your code; it was in the language’s willingness to guess.

Around that original sin, two decades of cruft had accreted: print was a statement rather than a function, integer division silently truncated, and the old “classic classes” lingered alongside the new ones. Each was a known wart. None could be fixed without breaking someone.

The Response

Python 3.0 was a considered answer to every one of those pressures. str became text — real unicode, everywhere — and bytes got their own honest type, with no silent bridge between them. print became print(), a function like any other. Division stopped truncating. Iterators and views replaced eagerly built lists. Classic classes were gone. You can defend every one of those decisions on its merits, and the maintainers did.

Each change was right. Together, all at once, they broke the world.

What followed was the long repair, and it took twelve years. First, a striking act of restraint: the moratorium (PEP 3003, 2009–11) froze language changes entirely so that the other Pythons — PyPy, Jython, IronPython — could catch up to 3.x. A language voluntarily standing still is a road not taken more often. 3.1 fixed the io speed problems. In 2011, PEP 404 put the schism’s endpoint in writing: there would be no Python 2.8, ever — in the PEP’s own words, “It is an ex-release.” No half-step, no back door. The only way out was forward.

Then came the peace offerings. Python 3.0 had removed the u'' prefix — the marker Python 2 code used to say “this string is unicode” — on the logic that in 3.x every string was unicode, so the prefix was noise. Correct, and disastrous: it meant a single file could not be valid in both languages. Python 3.3 restored u'' (PEP 414, authored by Armin Ronacher and Nick Coghlan), a change whose entire purpose was to do nothing — it exists so old code can run unmodified, a syntax feature as olive branch. The same release brought PEP 393’s flexible string representation. 3.4 shipped pip with every installation via ensurepip (PEP 453) — bundled, deliberately, rather than swallowed into the standard library, making packaging core’s problem at last. And 3.5 finally delivered the carrots: async/await, the @ matrix operator, typing. For the first time, Python 3 had things Python 2 users actively wanted.

The migration tooling told its own story in two acts. The official plan was 2to3, a translator: run it over your Python 2 source and it emits Python 3 source. A one-way trip — and that is why it failed. Libraries could not take a one-way trip while their users were still on 2; they would have had to maintain two diverging codebases forever. What won instead was the unofficial strategy: single-source straddling, writing one codebase that runs on both interpreters at once, papering over the differences with a compatibility library pointedly named six (2 times 3). Less elegant, endlessly pragmatic — code could creep toward the future without abandoning the present. A site called python3wos, the “wall of superpowers,” turned the migration into a public scoreboard, tracking which major libraries had crossed.

The end came slowly, then all at once. Python 2.7’s end-of-life was pushed five years, to 2020 — announced around PyCon 2014, written into PEP 373 — an amnesty that acknowledged reality. The final release, 2.7.18, shipped on April 20, 2020. pip 21.0 dropped 2.7 support in January 2021. And macOS, the last major holdout still shipping Python 2 by default, finally removed it in 12.3, March 2022. Nearly a decade and a half after 3.0, the second Python was gone.

The Fight

Why did it take twelve years? Not because anyone was lazy, and not because Python 3 was bad. It took twelve years because the ecosystem locked itself into a deadlock with no villain in it. Application developers could not move to Python 3 until the libraries they depended on ran there — nobody rewrites their app onto a platform where its dependencies do not exist. Library maintainers, volunteers almost to a person, could not justify porting for an audience of nobody — why spend nights and weekends migrating code for users who have not arrived? Each side was waiting, rationally, for the other to move first. That is a chicken-and-egg deadlock, and its signature is that everyone is behaving sensibly while the system as a whole goes nowhere. The mailing-list threads repeat it like a chorus: users wait for libraries, libraries wait for users.

From:
Paul Moore (CPython core developer)
Date:
November 3, 2009
Subject:
Re: [Python-Dev] 2.7 Release? 2.7 == last of the 2.x line?

“I don’t use Python 3, so I don’t test the new features, so the projects I need see little take-up, so I can’t use them in Python 3, so I don’t use Python 3…”

→ primary source

The loudest late battle came in November 2016 — eight years in, with the deadlock finally cracking. Zed Shaw published “The Case Against Python 3 (For Now),” arguing the migration was broken. The next day, Eevee published a point-by-point rebuttal. Read together, they are the schism in miniature: exhaustion on one side, exasperation on the other, and real grievances underneath both.

From:
Zed Shaw
Date:
November 22, 2016
Subject:
The Case Against Python 3 (For Now)
→ primary source
From:
Eevee
Date:
November 23, 2016
Subject:
A Rebuttal For Python 3
→ primary source

What actually ended the argument was not rhetoric but proof — migration war stories told from the summit once it was climbed. At PyCon 2017, Lisa Guo and Hui Ding keynoted on how Instagram moved its enormous Django codebase, live, without stopping the product. Dropbox ported more than a million lines incrementally, leaning on mypy’s type checking to make the crossing survivable. When companies at that scale said the water was fine, the excuses ran out.

From:
Lisa Guo & Hui Ding
Date:
PyCon 2017
Subject:
Python @ Instagram — keynote
→ primary source
From:
Dropbox
Date:
February 6, 2019
Subject:
Incrementally migrating over one million lines of code from Python 2 to Python 3
→ primary source

Why Your Code Looks Like This

Why are b'' and .encode()/.decode() everywhere in modern Python? Because the str/bytes split is the schism’s monument. Every explicit .encode() call is the language refusing, permanently, to guess an encoding for you — the guess is what buried the landmines. Why does print have parentheses? You know why now.

But the deepest mark is cultural, and you can see it in how Python changes today. Deprecation is now glacial: features are marked, warned about, and escorted out over many releases. from __future__ imports are a way of life — a standing mechanism for opting into tomorrow one file at a time, so that no one ever again wakes up to a language that broke overnight. The community carries the schism as a permanent trauma with a one-line creed: never again a big-bang break. It is why Guido frames the future as “there will never be a Python 4.”

And Python was not alone in the lesson. Other languages carry scars from the same era: PHP 6, the major version that was abandoned before release; Perl 6, the successor that diverged so far it had to become a different language named Raku. Big-bang breaks, it turns out, are how languages fork, stall, or die. Python paid twelve years to learn that and lived.

How long did the schism actually last? As late as 2019 — eleven years after 3.0 — JetBrains’ developer survey still found 10% of Python developers actively using Python 2.

Sources