Python Bytes: #499 So many questions??

Wait 5 sec.

Topics covered in this episode:PEP 824 brings ?? and ??= to Python for None handlingPython 3.15 gets a surprise RC3, and Python 3.10 reaches end of lifeasyncio.shieldPyxel: the retro game engine for PythonExtrasJokeWatch on YouTubeAbout the showSponsored by us! Support our work through:Our courses at Talk PythonConsulting from Six Feet UpConnect with the hostsMichael: Mastodon / BlueSky / X / LinkedInCalvin: Mastodon / BlueSky / X / LinkedInShow: Mastodon / BlueSky / XJoin us on YouTube at pythonbytes.fm/live to be part of the audience. Usually Tuesday at 7am PT. Older video versions available there too.Finally, if you want an artisanal, hand-crafted digest of every week of the show notes in email form? Add your name and email to our friends of the show list, we'll never share it.Michael #1: PEP 824 brings ?? and ??= to Python for None handlingPEP 824, a new draft from Marc Mueller sponsored by Guido van Rossum, proposes None-coalescing operators for Python 3.16. The ?? operator returns its left-hand side unless that value is None, and ??= assigns a fallback only when the target is None. It revives the long-deferred PEP 505 from over a decade ago, trimmed to just these two operators, with None-aware access split off into PEP 823. The goal is replacing verbose is None checks with something closer to or, but keyed on None instead of truthiness.Every Python codebase has piles of if x is None fallback code, and this is the first serious move in ten years to give it real syntax.user.age ?? "unknown" is like or but only falls back on None, so 0, "", and [] pass through instead of getting clobbereduser.name ??= "unknown" replaces the two-line check-then-assign pattern, with left-side subexpressions evaluated and cached exactly oncePrecedence sits between or and conditional expressions, matching JavaScript and C#, and ?? lands as a BoolOp in the AST next to and and orUnlike +=, ??= is a conditional assignment that skips the right side entirely, which is why it gets its own AST node rather than AugAssignStill draft status targeting 3.16, with a working reference implementation and an online demo to try, and it notably rejects a soft keyword like otherwise in favor of the familiar ??Discussion-worthy: the PEP answers the old objection that making None easier will proliferate its use, pointing out None is already everywhere and the status quo just produces uglier code# OG wayuser = db.get_user()city = Noneif user is not None: if user.address is not None: if user.address.city is not None: city = user.address.city# Michael's preferred wayuser = db.get_user()city = user?.address?.cityCalvin #2: Python 3.15 gets a surprise RC3, and Python 3.10 reaches end of lifeThe release team added a surprise third release candidate for Python 3.15 to fix last-minute lazy-import release blockers. They wanted time to test the fixes properly.That pushes the 3.15.0 final release from October 2 to October 9, 2026.RC3 has about 156 fixes from 82 contributors. The headline features are explicit lazy imports (PEP 810), the new frozendict (PEP 814) and sentinel (PEP 661) built-ins, and UTF-8 as the default encoding (PEP 686).No more ABI changes are coming, so library authors should be building 3.15 wheels now. uv 0.12.23 already added CPython 3.15.0rc3.On October 1, Python 3.10.22, 3.11.17, 3.12.15, 3.13.16 and 3.14.8 shipped together with nine common security fixes. They cover SSL validation, tarfile and zipfile handling, and urllib credentials.Python 3.10.22 is the final 3.10 release. After five years it gets no more security updates, so anyone still on 3.10 should upgrade.Python 3.13.16 is the last full maintenance release of 3.13, which now moves to security-only fixes. Only 3.13.16 and 3.14.8 have Windows and macOS installers, and the older releases are source-only.Python 3.11 gets security fixes through October 2027, and 3.12 through October 2028.A natural way to tie the two together: one version is nearly out the door and one is officially gone, and the same week is the time to test your code on 3.15 and move off 3.10.Michael #3: asyncio.shieldWhere async code can be cancelledIn async Python, every await is a point where your coroutine can be stopped. If someone cancels the task, asyncio raises CancelledError at whichever await the coroutine is paused on, and nothing after that line runs.Quart does this on purpose. When the client disconnects, it cancels the request's task. From quart/asgi.py:elif message["type"] == "http.disconnect": self._disconnected = True request.body.disconnect() request_task.cancel()That's reasonable for a slow page nobody is waiting on anymore. It's a problem for a handler that writes to the database in two steps:await downloads.insert_one(event) # 1. log the downloadawait totals.update_one(..., {'$inc': ...}) # 2. bump the episode's totalA podcast app requests the MP3, gets what it needs, and hangs up. If it hangs up while step 2 is in flight, step 1 has already landed and step 2 never runs. Nothing is logged and nothing reaches Sentry, because a cancelled request isn't an error. The two collections just drift apart.Under WSGI this couldn't happen. A disconnect didn't stop your view; the view finished both writes and the server only failed later, when it tried to send the response.The demoI ran this on Python 3.14. The "client" hangs up 15 ms into a pair of 10 ms writes:async def record_download(): await db_write('events') await db_write('total')_pending: set[asyncio.Task] = set()async def record_download_shielded(): task = asyncio.create_task(record_download()) _pending.add(task) task.add_done_callback(_pending.discard) await asyncio.shield(task)record_download events=1 total=0record_download_shielded events=1 total=1The unshielded version loses the increment. The shielded version finishes it, even though the request was cancelled. The full script is in the scratchpad as shield_demo.py if you want it for the show.What asyncio.shield does, and its trapsshield(task) protects the inner task from a cancellation aimed at the outer one. The handler still gets CancelledError immediately, so nothing after that line runs, but the inner task keeps going until both writes finish. The traps:Keep a reference to the task. The event loop only holds weak references to tasks, so a task with no other reference can be garbage-collected mid-run. The asyncio docs warn about this. That's what _pending is for.Its errors go nowhere by default. If a shielded write fails after the handler has already been cancelled, nobody awaits the result. You only get a "Task exception was never retrieved" warning. Log errors from inside the task, or in the done callback.It doesn't survive a process shutdown. If the event loop is closing, as during a Granian worker recycle or a deploy, pending tasks are still cancelled. The window is tiny, but it isn't zero.It hides hangs. A shielded write to a stuck database lives forever. Put a timeout inside the shielded task (asyncio.timeout(...)), not outside it.It makes the pair finish, not atomic. A crash between the two writes still splits them. The real fix is one operation that can't half-happen. That's what the SQLite move gets: an insert and an upsert in one synchronous transaction, with no await in the middle. Shield is the right patch for the Mongo code until then.The general lesson: in async code, related writes made one after another are only as reliable as the client's patience. Before shipping something like that, check whether your framework cancels on disconnect. Quart does, and I believe Django 5's async views do too. I don't think Starlette cancels a plain endpoint, but I haven't checked any of these other frameworks' source, so verify them before you say it on air.Calvin #4: Pyxel: the retro game engine for PythonPyxel is a free, MIT-licensed retro game engine for Python, built and maintained by a single developer. It has been in development since 2018 and has passed 18,000 GitHub stars.It's modeled on classic game consoles and deliberately limits you to 16 colors and 4 sound channels, which keeps scope small and games finishable.Built-in editors cover pixel art, tilemaps, sound and music, so you can make the graphics, audio and game logic in one place.The engine is implemented in Rust and compiled to WebAssembly, but you write your games in plain Python.Pyxel Code Maker is a browser playground with a code editor, resource tools and a run button. It loads projects from local files, GitHub Gists or URLs, and saves back to a Gist for sharing.Games can also run on the web, so sharing one can be as simple as sending a link.It's a fun, low-barrier way into game development, good for beginners and for teaching kids.ExtrasCalvin:Python Lang Summit 2026 discusses a sys namespaceBe Careful Updating Your RefrigeratorMichael:New Siri is No BuenoWho out there is doing interesting work with sqlite and litestream? Trying to put an episode together on it.Tinymongo shoutout https://tinymongo.orgJoke: I was there.