Why I'm writing this
If you've ever thought contributing to Python is only for core developers with years of C experience — I felt the same way. Then I fixed a real bug in the standard library, went through review with Serhiy Storchaka, and PR #151120 merged into CPython.
This post is the path I walked — not a victory lap, but a map you can follow.
The bug (in plain English)
Creating an email with Japanese text and shift_jis charset crashed when you tried to print the message:
from email.message import EmailMessage
from email.contentmanager import raw_data_manager
m = EmailMessage()
raw_data_manager.set_content(m, "日本語\n", charset="shift_jis")
print(m) # UnicodeEncodeError in Python 3.11+
Same issue for euc-jp. It worked in Python 3.10 and broke after a regression in 3.11.9.
Issue: python/cpython#150771
How I found it
I was already contributing to CPython — reading issues, building locally, running tests. This one stood out:
- Clear reproducer (copy-paste and it crashes)
- Small scope (stdlib
emailmodule, pure Python) - A maintainer (Serhiy) had already described the expected fix
That's the pattern I'd recommend for a first PR: small, reproducible, with maintainer guidance.
The root cause — two charsets
Some email charsets have an input name and a different output name:
| You pass in | Encoded as (output) | Content-Type header |
|---|---|---|
shift_jis |
iso-2022-jp |
iso-2022-jp |
euc-jp |
iso-2022-jp |
iso-2022-jp |
The buggy code used input_charset when encoding the body, but the Content-Type header used output_charset. The payload and header disagreed from the start — so str(m) crashed when serializing.
My first fix was wrong (and that's OK)
My first approach patched the serialization layer so print(m) wouldn't crash. It worked locally, but Serhiy called it out: it "sweeps the problem under cover." The message was still stored with the wrong encoding internally.
Lesson: When a maintainer says fix it at the source, listen. A one-line root fix beats a clever wrapper.
The final fix — one line
In Lib/email/contentmanager.py, function set_text_content:
# Before (bug)
charset = email.charset.Charset(charset).input_charset
# After (fix)
cs = email.charset.Charset(charset)
charset = cs.output_charset
That's the entire production change in contentmanager.py. Plus tests and a NEWS entry.
Net diff: 3 files, ~46 lines added — mostly tests.
What the review process taught me
| Step | What happened |
|---|---|
| Open PR | Jun 9, 2026 |
| Maintainer feedback | Serhiy asked for root fix, not serialization patch |
| Rework | Reverted wrapper, applied one-liner, added tests |
| CI surprise | Docs job failed on a broken :func: reference in NEWS — fixed with plain backticks |
| Review polish | Added bytes(m) assertions to match existing test style |
| Merged | Jun 16, 2026 |
Patience matters. I waited for direction before pushing the rework, ran test_email locally (1815 tests), and learned that CPython has a separate Docs CI job — not just unit tests.
How you can start
- Read the Python Developer\'s Guide
- Pick issues with a clear reproducer and small scope (docs, tests, stdlib Python)
- Build CPython locally and run the relevant test module before opening a PR
- When review asks for changes, fix the root cause — don\'t argue for the shortcut
- Expect iteration: my branch had several commits before the net diff was clean
Links
- Merged PR: https://github.com/python/cpython/pull/151120
- Issue: https://github.com/python/cpython/issues/150771
- Files changed:
Lib/email/contentmanager.py,Lib/test/test_email/test_contentmanager.py, NEWS
I'm working on more CPython contributions — follow along here on StackLog.