3.15rc1 Froze the ABI. Ruff Is Already Rewriting Your except

Real Python's September roundup locks 3.15's binary interface. Ruff 0.15 will strip except parentheses on 3.14. Pin the toolchain before the formatter does it in CI.

September’s Python news is mostly about doors closing.

Real Python’s monthly roundup leads with three releases, seven new PEP drafts, a rejection, and a withdrawal. Two of the releases matter because they stop something rather than start it. One freezes the 3.15 application binary interface for the rest of the series. The other patches holes that sat open longer than anyone wanted. The fun part, they say, is the PEPs. The operational part is your manylinux wheel and the Ruff version in CI.

This is not last week’s Hugging Face acquisition note. That was a vendor. This is the interpreter and the formatter disagreeing about parentheses.

3.15rc1 is a date for extension authors

Python 3.15.0rc1 landed on August 4, on the schedule August’s column had already printed. Feature freeze was in May. The headline now is the ABI. From rc1, the contract between compiled extensions and the interpreter is locked. Only reviewed bug fixes land.

If you ship a C extension, Real Python’s line is blunt: you have until October to find out whether the build works. That is not drama. That is cibuildwheel against 3.15rc1 on the same runners you use for 3.12, plus a free-threaded build if you claim you support it.

What 3.15 has been assembling, per that roundup: frozendict, sentinel, explicit lazy imports, UTF-8 by default, unpacking in comprehensions, a JIT they clock at roughly 8 to 9 percent on x86-64 Linux, and official macOS installers that ship the free-threaded build by default. The last one is the footgun for anyone who tests on the macOS installer and assumes they got a GIL build.

Quansight’s CPython ABI tour is the piece Real Python still points at if you need the three-letter contract unpacked. We are not going to re-explain stable ABI here. The checklist is smaller:

  • Build wheels for 3.15rc1 now, not after the final.
  • If you use the limited API, confirm the version macro you compiled with.
  • If you ship a free-threaded wheel, test it on the macOS default, not only on a Linux container you configured by hand.
  • Do not treat an 8 to 9 percent JIT number as a reason to skip profiling. It is an x86-64 Linux figure from the column, not your allocation-heavy service.

There is also a security release across three branches, and a note about two lines that crash the interpreter. If you pin patch versions in Docker images, this is the week you bump and rebuild, not the week you argue about new syntax.

3.14 is not done with you

PEP 745 is the release schedule, Hugo van Kemenade as release manager. 3.14.0 final was 7 October 2025. 3.14.7 went out 5 August 2026. Next expected bugfix: 3.14.8, Tuesday 6 October 2026. Bugfixes about every two months for about 24 months. Source-only security fixes until about October 2030.

If production is on 3.14, you are still in the bugfix window. You are not on a museum version. You also do not need to jump to 3.15 the week the ABI freezes. Extension authors need 3.15rc1. Application teams need 3.14.8 on the calendar and a pip-compile or uv lock that will actually pick it up.

Ruff 0.15 will change your diffs

Astral’s Ruff v0.15.0 post is a style-guide release dressed as a formatter changelog.

PEP 758, in Python 3.14, allows except A, B, C without parentheses. Ruff’s formatter now removes those parentheses when target-version is 3.14 or later. If your CI formats on 3.14 and your colleagues still read code like it is 3.11, you will get a thousand-line diff that does not fix a bug.

The 2026 style guide, in the same post:

  • Lambda parameters stay on one line; the body gets parentheses so it can wrap. Closer to Black.
  • A single empty line after a function header or docstring is preserved. The 2025 style stripped it.
  • # fmt: skip used to apply to at most one node. If you were stacking skips, read the release notes before you upgrade.

This is a best-practice problem disguised as taste. Pin target-version. If the codebase still runs 3.12 in production, do not let the formatter pretend it is 3.14. If you are on 3.14 and you hate unparenthesized except, put the ignore in config on purpose, in git, not in a review comment that loses.

Run Ruff 0.15 on a branch with --diff before you let it touch main. Formatter releases that “align with Black” are how you spend Friday on noise.

mypy --strict without boiling the repo

On September 7, Tim Hopper’s pydevtools note restated the thing every team learns the hard way. Default mypy is lenient. It skips function bodies that have no annotations. --strict is a bundle of flags that changes between mypy releases. The post is verified against mypy 2.3 and uv 0.12.

The useful pattern is not “strict everywhere on Monday.”

New code, or a package that is already annotated: [tool.mypy] strict = true, then subtract the flag that is noise. Hopper’s example is warn_return_any = false.

Old code: keep the global config loose, then override.

[[tool.mypy.overrides]]
module = ["mypackage.api", "mypackage.api.*"]
strict = true

Third-party packages without stubs or a py.typed marker will error on import. That is mypy, not strict mode. Silence it per package with ignore_missing_imports = true on that module override. A global ignore is how you miss your own missing import.

If you adopted uv, put mypy in the dev dependency group and run it from the lockfile. “The laptop has mypy” is not a check.

Async yield from, and why it is not your incident

Real Python also covers async generators finally getting yield from. Delegation becomes the interpreter’s job. Values, exceptions, and close requests propagate the way they do in sync code, and StopAsyncIteration gets a .value so return means something.

That is a nice language change. It is not why your workers page. If you maintain a library that implemented the delegation by hand, read the PEP and add 3.15 to the test matrix. If you write application code that just async fors, you can wait for the final.

The same column mentions free threading meeting real workloads, two elections, and a joke about eleven ways to end a line. Skim those after you bump the image. Community news is not a pin.

A pyproject skeleton that does not fight itself

Keep the interpreter, the formatter, and the type checker looking at the same floor.

[project]
requires-python = ">=3.12,<3.16"

[tool.ruff]
target-version = "py312"

[tool.mypy]
python_version = "3.12"

If production is 3.14, say 3.14 in all three places and accept that Ruff 0.15 may strip except parens. If production is 3.12, do not let a developer laptop on 3.14 reformat the tree because the default target followed the local interpreter.

uv 0.12, in Hopper’s verification line, is old enough that “we use uv” is no longer an excuse for an unpinned mypy. Put the versions in the lock. Review the lock when Ruff jumps a minor.

What to do this week

  1. If you publish C extensions, add 3.15rc1 to the wheel matrix. October is the deadline Real Python printed, not a feeling.
  2. If you run 3.14 in prod, note 6 October for 3.14.8. Do not skip 3.14.7 if the image is older than 5 August.
  3. If CI uses Ruff, read 0.15 before it formats main. Set target-version to the interpreter you actually deploy.
  4. If you have been meaning to turn on mypy strict, do it on one package path, not the monorepo.
  5. Security releases and interpreter crashes in the Real Python list are patch-and-rebuild, not a Slack poll.

We already had a week where Langflow’s Python exec hole and Starlette’s BadHost on KEV were the lesson. Those were other people’s CVEs. This month’s best practice is more boring: freeze the ABI, freeze the formatter, freeze the type-checker flags, and stop letting a default target-version rewrite the repo because PEP 758 exists.

The PEPs can stay fun. The lockfile should not.

If you maintain a C extension that still ships a single cp312 wheel and hopes, rc1 is the embarrassment rehearsal. Users on 3.15 will pip-install a pile of build logs. That is on you, not on Hugo van Kemenade’s calendar.

Application teams can ignore most of the 3.15 toy list until a library they import starts requiring it. frozendict and sentinel are not reasons to move prod in September. The ABI freeze is a reason to stop telling extension maintainers that “next month is fine” without a wheel. Next month is October. The column already used that word.

UTF-8 by default and unpacking in comprehensions will show up in examples and then in a junior’s pull request. Decide the language level in the style guide, not in the review. If the guide still says 3.12, reject the 3.15-only syntax even if it looks cleaner. Formatter and interpreter have to agree. That was the whole article. Pin it. Then go home. Tomorrow you can argue about frozendict. Tonight you bump the patch release and leave the formatter alone. That is enough.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.