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 email module, 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

  1. Read the Python Developer\'s Guide
  2. Pick issues with a clear reproducer and small scope (docs, tests, stdlib Python)
  3. Build CPython locally and run the relevant test module before opening a PR
  4. When review asks for changes, fix the root cause — don\'t argue for the shortcut
  5. 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.