Planet Python
Last update: July 11, 2026 09:47 PM UTC
July 10, 2026
Mike Driscoll
An Intro to Spiel – Creating Presentations in Your Terminal with Python
Have you ever wanted to create a presentation in your computer’s terminal? While this is an uncommon need, a clever open source developer has provided a solution to this problem! The project is called Spiel, and while it is currently archived, the idea is pretty cool. Spiel uses the Rich package to create the slides […]
The post An Intro to Spiel – Creating Presentations in Your Terminal with Python appeared first on Mouse Vs Python.
Talk Python to Me
#554: Trustworthy AI in Healthcare and Longevity
You ask an AI a question and it answers with total confidence. Most of the time, a confidently wrong answer is just an annoyance. But what if the question is medical, and there's a real patient on the other end? In that world, a hallucination isn't a bug, it's a patient-safety event. Sumit Gundawar is a London-based software engineer who builds the clinical platform for a UK longevity and aesthetic-medicine clinic, and his whole argument is that in high-stakes AI, the model is the easy part. Earning trust is the real engineering. We dig into grounding, refusal logic, human-in-the-loop design, and the messy frontier of longevity and biohacking, plus a live demo of an assistant that refuses to answer when it can't back up the claim. Let's get into it.
July 09, 2026
EuroPython
Humans of EuroPython: Daria Linhart Grudzień
EuroPython wouldn&apost exist without the wonderful volunteers who pour countless hours into organising it. From contracting the venue to selecting and confirming talks and workshops, hundreds of hours of loving work go into making each edition the best one yet.
Join us in celebrating one of the humans
Python Software Foundation
The PSF D&I Workgroup Are Starting Office Hours in July!
July 08, 2026
Django Weblog
Last Call 2026 Django Developer Survey
Time is running out. This is the last call for the 2026 Django Developers Survey, which the Django Software Foundation is running in partnership with JetBrains.
The survey closes on July 13, 2026. It is one of the best measures we have of how Django is used, and it helps guide future technical and community decisions.
So far, over 3,100 people have responded, and we would love to push that number past 4,000. Every response helps us better understand the Django community.
This year's survey was shaped by the Django Steering Council, the Django Fellows, the Django Software Foundation Board of Directors, and several community members. Your feedback helps us understand your needs, see how you use Django, and plan for future development and community needs.
How you can help
Once you’ve done the survey, take a moment to re-share on socials and with your communities. The more diverse the answers, the better the results for all of us. We appreciate everybody helping to get the word out.
Please use the following links:
-
Bluesky
https://surveys.jetbrains.com/s3/bs-django-developers-survey-2026 -
Django Forum
https://surveys.jetbrains.com/s3/df-django-developers-survey-2026 -
LinkedIn
https://surveys.jetbrains.com/s3/li-django-developers-survey-2026 -
Mastodon
https://surveys.jetbrains.com/s3/md-django-developers-survey-2026 -
Reddit
https://surveys.jetbrains.com/s3/r-django-developers-survey-2026 -
X / Twitter
https://surveys.jetbrains.com/s3/x-django-developers-survey-2026
For more details, read the original announcement on the Django blog.
Mike Driscoll
New Book Release: Python Typing
I am happy to announce that my latest book, Python Typing, is now available on all platforms. You can get your copy on Gumroad or Leanpub or Amazon Python has had type hinting support since Python 3.5, over TEN years ago! However, Python’s type annotations have changed repeatedly over the years. In Python Typing: Type Checking […]
The post New Book Release: Python Typing appeared first on Mouse Vs Python.
Hugo van Kemenade
Fixing the dictionary with Python 3.14
Yes, but not the
dict kind of
dictionary.
When working on CPython, we often find obscure bugs elsewhere, in compilers, operating systems and elsewhere:
Since Python 3.8, the release notes have a section called “And now for something completely different”. These have included Monty Python sketches, astrophysics facts and poetry.
For Python 3.14, I’m doing all things π, pie and [mag]pie (more here). As part of the research for this important task, I looked up pi in the Oxford English Dictionary.
As we all recall from the Python 3.14.0b1 release notes, William Jones was the first person to use the π symbol to denote the circle’s circumference to its diameter in his Synopsis Palmariorum Matheseos (1706):
However, the OED’s first citation had a markup bug:
I duly reported this to the OED in July 2024; and by the next time I looked it up, in June 2025, it was fixed!
Hooray!
Header photo: Part of the definition for “get” in the OED’s 1901 forerunner, A New English Dictionary on Historical Principles (CC BY-NC-SA 2.0 Hugo van Kemenade).
Marc-André Lemburg
My 25th EuroPython - in a row😊
Next weekend, I&aposll be heading to Kraków, Poland, for my 25th EuroPython conference.
It&aposs been a long ride since the first EuroPython conference in Charleroi, Belgium, but one I wouldn&apost have wanted to miss.
This year, I&aposll be giving a
Python GUIs
Why Widgets Appear as Separate Windows — Understanding widget parenting in Qt and how to fix widgets that float outside your main window
Sometimes when I dynamically add widgets to tabs in my PyQt6 application, they pop out as windows instead. What's going on?
July 07, 2026
Brett Cannon
How to publish to PyPI using GitHub Actions securely
There have been several security incidents lately that involved compromising GitHub Actions workflows. This has led some to say "GitHub Actions is the weakest link" in publishing and to GitHub publishing a GitHub Actions security roadmap update. But saying it&aposs an issue and acknowledging the fact
PyCoder’s Weekly
Issue #742: Wagtail as Admin, Random Values, Code Quality, and More (2026-07-07)
PyCharm
Best Object Detection Models for Machine Learning in 2026
Object detection powers transformative applications, from autonomous vehicles navigating city streets and security systems identifying threats in real time to retail analytics tracking inventory and medical imaging detecting tumors. But choosing the right model for your computer vision project can be challenging, especially with dozens of architectures claiming superiority across different metrics. In this guide, […]
Ari Lamstein
This Thursday: Building Data Apps with Streamlit and Copilot
On July 9 (9am–1pm Pacific) — this Thursday — I’ll be teaching a 4-hour live workshop for O’Reilly: Building Data Apps with Streamlit and Copilot. This is the second time I’ve run this workshop, and I’ve made several improvements based on what I learned the first time. If you work in Python and want to […]
Django Weblog
Django security releases issued: 6.0.7 and 5.2.16
In accordance with our security release policy, the Django team is issuing releases for Django 6.0.7 and Django 5.2.16. These releases address the security issues detailed below. We encourage all users of Django to upgrade as soon as possible.
CVE-2026-48588: Potential exposure of private data via cached Set-Cookie response
django.middleware.cache.UpdateCacheMiddleware and django.views.decorators.cache.cache_page avoided caching responses that set a cookie while varying on Cookie only when the incoming request contained no cookies at all. When the request already carried an unrelated cookie (such as a language or theme preference cookie), the protection did not apply, allowing a response that sets a session or other sensitive cookie to be stored in Django's shared cache.
This issue has severity "low" according to the Django security policy.
Thanks to Chris Whyland for the report.
CVE-2026-53877: Heap buffer over-read in GDALRaster
When django.contrib.gis.gdal.GDALRaster was instantiated with a bytes object representing a raster file, the vsi_buffer property could over-read the allocated buffer by approximately 32 bytes. This could result in information disclosure of adjacent heap memory or, in rare cases, a segmentation fault. Only rasters stored in GDAL's virtual filesystem were affected.
This issue has severity "low" according to the Django security policy.
Thanks to Bence Nagy for the report.
CVE-2026-53878: Header injection possibility since DomainNameValidator accepted newlines in input
django.core.validators.DomainNameValidator accepted newlines in domain names. If such values were included in HTTP responses, header injection attacks were possible. Django itself wasn't vulnerable because HttpResponse prohibits newlines in HTTP headers.
The vulnerability only affected uses of DomainNameValidator outside Django form fields, as CharField strips newlines by default.
This issue has severity "low" according to the Django security policy.
Thanks to Bence Nagy for the report.
Affected supported versions
- Django main
- Django 6.1 (currently at beta status)
- Django 6.0
- Django 5.2
Resolution
Patches to resolve the issue have been applied to Django's main, 6.1 (currently at beta status), 6.0, and 5.2 branches. The patches may be obtained from the following changesets.
CVE-2026-48588: Potential exposure of private data via cached Set-Cookie response
- On the main branch
- On the 6.1 branch
- On the 6.0 branch
- On the 5.2 branch
CVE-2026-53877: Heap buffer over-read in GDALRaster
- On the main branch
- On the 6.1 branch
- On the 6.0 branch
- On the 5.2 branch
CVE-2026-53878: Header injection possibility since DomainNameValidator accepted newlines in input
- On the main branch
- On the 6.1 branch
- On the 6.0 branch
- On the 5.2 branch
The following releases have been issued
The PGP key ID used for this release is Jacob Walls: 131403F4D16D8DC7
General notes regarding security reporting
As always, we ask that potential security issues be reported via private email
to security@djangoproject.com, and not via Django's Trac instance, nor via
the Django Forum. Please see
our security policies for further
information.
PyCon
Welcome, Kattni!
Python Bytes
#487 Minimum requirements
Topics include dust, Hermes Agent, and llm-coding-agent 0.1a0.
The No Title® Tech Blog
Optimize Images X now supports WEBP and drag and drop
Optimize Images X, the multi-platform desktop application that helps you reduce the file size of your images on macOS, Windows and Linux, has just been updated to version 2.1.0. This new version adds drag-and-drop, conversion to more output formats, including WebP and AVIF, a new image info window with a readable EXIF report, and a PyInstaller spec file for building standalone executables.
Seth Michael Larson
Playing the “Second Quest” of Legend of Zelda: Four Swords Adventures
July 06, 2026
Rodrigo Girão Serrão
Write a coding agent from first principles: better tools
Improve the capabilities of your agent by providing it with better tools.
Introduction
This tutorial builds on the coding agent you implemented in the tutorial “Write a coding agent from first principles”. In this tutorial, you'll take your agent and improve its capabilities by implementing the text edit and bash command tools that Anthropic provides.
Why use Anthropic's tools?
In the previous tutorial you implemented a coding agent that has a few tools that it can use to read, write, and execute, code.
The tool "bash" can be used to execute arbitrary commands and the tools "read", "write", "replace", and "insert", can be used to edit files.
As it turns out, these tools are so universally useful that Anthropic trained its models on specific schema definitions for these tools. The tools still run on the client side, so you'll still get tool use blocks in the API responses, but you don't have to define the schema for the tool. You just specify the tools by their Anthropic types and names, and the LLMs will happily request tool uses.
Anthropic trains their models on a number of useful tools but you'll focus your attention on two tools that emulate the functionality you already have:
- Text editor tool: this tool replaces the four tools you defined to read, write, replace, and insert, text in text files
- Bash tool: this tool provides a persistent bash session that can run bash commands
By replacing your tools with Anthropic's, the agent will be able to make better tool calls consistently, since Anthropic trains their models on their specific tool schemas.
The native text editor tool
To define support for Anthropic's text editor tool you need to add it to your list of tools.
The name of the tool is "str_replace_based_edit_tool" and its type is "text_editor_20250728".
(The type carries a versioning suffix that may influence the tool's behaviour, so make sure you use the right date suffix.)
Since you'll be using Anthropic's text editor tool, you can delete the functions read, write, replace, and insert, and the corresponding dictionaries that go in the list TOOLS.
Instead, add the dictionary that specifies the Anthropic tool:
# ...
TOOLS = [
{
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool",
}
]
# Bash tool defined and added later.
For organisation purposes, you'll define the text editor tool and the bash tool in their own submodules, so create the folder tools and then create the file tools/str_replace_based_edit_tool.py under src/agent.
In there, you'll define the code to handle the tool call.
The text editor tool is a 4-in-1 tool that allows you to view, replace, create, and insert, text. To disambiguate the action you want to do, the tool use request includes a command:
# Example tool use dictionary:
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "str_replace_based_edit_tool",
"input": {
"command": "view", # <--
# ...
}
}
You'll use the key "command" from the...
Seth Michael Larson
Mario Kart World and “seamless” media
July 05, 2026
Christian Ledermann
Migrate From mypy To ty And pyrefly
I wanted to migrate one of my Python packages from mypy to ty and pyrefly. I handed this task over to Claude, and at the end I asked it to write out some guidance on how to perform it most efficiently. So what follows is AI-generated 'slop'.
This guide is not about using fastkml. It documents how fastkml itself was migrated from mypy to Astral's ty and Meta's pyrefly, so the same playbook can be replayed on other codebases with less trial and error. Keep it here because the next migration (human- or agent-driven) should start from findings, not from zero.
Running two checkers instead of one is deliberate, not incidental. ty and pyrefly disagree with each other and with mypy often enough that running only one gives a false sense of completeness. Budget for both, and expect them to catch different subsets of the same bugs.
TL;DR
- Get a raw error-count baseline for both tools before touching any source. Categorize by error kind, not by file.
- Look for one systemic root cause before fixing anything file-by-file. In most codebases with an optional C-extension backend (lxml, pydantic-core, orjson, etc.) there is a single architectural fix that collapses 60-90% of the noise.
- Fix genuine bugs the tools surface (there will be some — both tools are pickier than mypy about
Optional/union narrowing, positional-only stubs, and unpacking). - Bulk-suppress test-file "constructed-then-accessed-without-narrowing" noise scoped to
tests/**, not case by case, and not by widening the rule to error kinds that could hide real bugs (invalid-argument-typeis notunresolved-attribute). - Turn on strict presets, then promote specific rules the preset doesn't cover, and explicitly cut the ones that create disproportionate mechanical churn (ask a human before doing a 80-site
@overridesweep). - Verify: both tools clean, full test suite green (with and without optional runtime deps installed), linter clean, and the TOML re-parses.
Phase 0 — Inventory the mypy config honestly
Before deleting [tool.mypy], read what each flag actually bought you, because that's the strictness bar ty/pyrefly need to match or exceed:
| mypy setting | Rough ty/pyrefly equivalent |
|---|---|
disallow_any_generics |
ty missing-type-argument = "error"
|
warn_redundant_casts |
ty redundant-cast; pyrefly redundant-cast (both exist, check current default level) |
warn_unused_ignores |
ty unused-ignore-comment / unused-type-ignore-comment; pyrefly unused-ignore (on by default under strict) |
warn_unreachable |
pyrefly's strict preset covers this; ty has no exact analog — don't assume parity, spot-check |
disallow_untyped_defs |
Neither tool has a literal flag for this — ty infers types through unannotated bodies by default (different philosophy from mypy). Don't expect a 1:1 mapping; re-derive intent instead of hunting for the same flag name. |
Per-module disable_error_code overrides |
pyrefly [[tool.pyrefly.sub-config]] + matches glob; ty [[tool.ty.overrides]] + include glob |
Also inventory stale per-module overrides — a mypy config that's been edited over years accumulates dead entries (a module path that was renamed or deleted, but the override survived). Grep for the referenced paths; don't carry dead config forward.
Phase 1 — Install both tools and get a baseline
uv pip install ty pyrefly
ty check <src> <tests>
pyrefly check <src> <tests>
Immediately categorize, don't read line by line yet:
ty check <src> <tests> 2>&1 | grep -oE '^error\[[a-zA-Z-]+\]' | sort | uniq -c | sort -rn
ty check <src> <tests> 2>&1 | grep -E '^\s+-->' | sed -E 's/^\s+--> //; s/:[0-9]+:[0-9]+$//' | sort | uniq -c | sort -rn
The second command (errors by file) usually reveals the systemic cause immediately: a handful of files concentrate a disproportionate share of the errors, and they're usually the ones touching an optional/duck-typed dependency.
If a partial migration already exists in the repo (CI switched over but pyproject.toml grew broad ignore-missing-imports = ["*"] / blanket missing-attribute = "ignore" sub-configs), treat that as a red flag, not a starting point. Broad suppressions accumulated during an in-progress migration usually mean someone hit friction and silenced it rather than fixed it. Re-run with those suppressions removed to see the real baseline before deciding what's worth keeping.
Phase 2 — Find the systemic root cause first
The highest-leverage move in this kind of migration is almost never "fix errors file by file." It's finding the one architectural mismatch that both checkers are tripping over identically across dozens of call sites.
The recurring pattern: a project supports an optional, richer backend (lxml over xml.etree.ElementTree, orjson over json, ujson, a C-accelerated regex engine, etc.) via a runtime try/except import, and defines either a Protocol or just relies on structural duck-typing to abstract over both. mypy tolerated this for years via ignore_missing_imports = true, which silently treats the untyped backend as Any everywhere. Neither ty nor pyrefly degrade that gracefully by default — they'll either partially resolve the untyped backend's real (but incomplete) info, or fall back to typing it against whichever branch of the try/except does have full stubs (usually the stdlib fallback), and then report every method/kwarg the richer backend uniquely offers as invalid.
The fix, applied at the import site (not scattered across every call site):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Type-checkers see the richer backend's own stubs; the preferred
# backend's API is treated as a superset of the fallback's.
from lxml import etree
else:
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
If the project also defines its own Protocol to abstract over both backends (e.g. types.py: class Element(Protocol): ...), consider going one step further and making that Protocol literally alias the richer backend's real type under TYPE_CHECKING, falling back to the structural Protocol only for runtime/non-typechecking purposes:
if TYPE_CHECKING:
from lxml.etree import _Element as Element
else:
class Element(Protocol):
... # the original structural protocol, unchanged
This one change collapsed roughly 150 of ~240 diagnostics in the fastkml migration, because it fixed both the "backend-specific kwarg doesn't exist" class of errors and the "structural Protocol isn't assignable to a concrete stdlib parameter type" class in one shot (see pitfall below).
If a stub package exists for the richer backend (lxml-stubs, types-ujson, etc.), add it to your typing dev-dependencies — but read the pitfalls section before assuming it's a strict improvement for both checkers.
Pitfalls (the part worth re-reading before your second migration)
1. # type: ignore[code] is not portable
Neither ty nor pyrefly parses mypy's bracketed error-code suppression the way mypy does.
-
tyhonors a bare# type: ignore(no brackets) as a blanket suppression for that line, but a bracketed# type: ignore[some-code]is not recognized as a ty-ignore at all — it's inert noise as far as ty is concerned, and the underlying error still fires. -
pyreflydoes honor# type: ignore[...]by default (its--enabled-ignoresdefaults totype,pyrefly), so bracketed mypy comments mostly still work for pyrefly specifically. - Both tools have their own dedicated syntax:
# ty: ignore[rule-name]and# pyrefly: ignore/# pyrefly: ignore[error-kind].
Verify empirically before trusting any of this — behavior can change between tool versions:
def f() -> int:
return "y" # type: ignore[return-value]
Run ty check and pyrefly check against a two-line repro before deciding on a suppression strategy for the whole codebase.
Practical rule that worked well: keep the original # type: ignore[code] comment (documents intent, keeps pyrefly happy) and append # ty: ignore[rule-name] on the same physical line for ty. Don't strip the mypy-era comments outright; they're free documentation of why a line is exceptional.
2. pyrefly's TOML keys are snake_case even though its CLI flags are kebab-case
This is the single most time-consuming mistake to make. pyrefly check --replace-imports-with-any 'lxml.*' works from the CLI. Writing the "obvious" TOML equivalent:
[tool.pyrefly]
replace-imports-with-any = ["lxml.*"] # WRONG — silently different key
...does not raise an error from pyrefly check in some code paths, but it does hard-fail with pyrefly dump-config (unknown variant 'replace-imports-with-any'... Fatal configuration error), and depending on invocation order this can also break pyrefly check itself later. The correct TOML key uses underscores:
[tool.pyrefly]
replace_imports_with_any = ["lxml.*"]
Meanwhile, error-kind names (used as dict keys under [tool.pyrefly.errors] or inside a rules = {...} table) do use hyphens (missing-override-decorator, redundant-cast, etc.) — matching the CLI's --error/--ignore rule-name spelling, not the config-field spelling. There is no single consistent casing convention across the whole config surface; check pyrefly dump-config after every config change, not just pyrefly check, because check can look clean while a nearby key is silently ignored.
After any pyrefly config edit, run both pyrefly dump-config (schema/parse validation) and pyrefly check (behavioral validation) — one catches structural mistakes the other doesn't surface.
3. pyrefly's [[tool.pyrefly.sub-config]] array-of-tables is fragile against interleaving
Pyrefly's per-path overrides use TOML's array-of-tables syntax:
[[tool.pyrefly.sub-config]]
matches = "tests/**/*"
[tool.pyrefly.sub-config.errors]
missing-attribute = "ignore"
TOML allows other, unrelated top-level tables to appear between [[tool.pyrefly.sub-config]] and its paired [tool.pyrefly.sub-config.errors] — the nested table still binds to the most-recently-opened array element regardless of what's interleaved. That means a pyproject.toml that grew organically (auto-migration tooling appending blocks near whatever happened to be at the end of the file) can end up with three sub-config blocks scattered across 100+ lines of unrelated project/tool config, and it will still parse. It becomes a landmine the moment someone (or an agent) deletes one [[tool.pyrefly.sub-config]] header without also deleting its now-orphaned [tool.pyrefly.sub-config.errors] block — the orphaned errors table then either binds to the wrong array element or breaks parsing entirely.
Fix: keep every pyrefly (and ty) config block contiguous in one place in the file, even if that means moving it away from wherever an automated tool first inserted it. Re-parse after every edit:
python3 -c "import tomllib; tomllib.load(open('pyproject.toml','rb')); print('OK')"
4. A Protocol is not assignable to a concrete class parameter
If your duck-typing abstraction is a Protocol (say, types.Element) and internal code passes Element-typed values into functions that are typed against the concrete stdlib/third-party class (xml.etree.ElementTree.SubElement(parent: Element[Any], ...)), both ty and pyrefly will reject it — even though the Protocol is structurally compatible at every call site. Protocol → concrete-class assignability doesn't work the way concrete → Protocol does, and a mutable/invariant attribute (text: str on the Protocol vs. text: str | None on the real class) makes it worse.
This resolves itself for free once you apply the Phase 2 fix (alias the Protocol to the real backend's type under TYPE_CHECKING) for internal, backend-facing code. Keep the original Protocol only for the codebase's genuinely-public, backend-agnostic API surface.
5. Registry/callback-style dispatch can't be narrowed at the signature level
A common pattern: a generic dispatch table stores classes: tuple[type[object], ...] and a matching Protocol requires every registered callback to accept exactly that (necessarily wide) signature, even though any individual callback only ever receives one concrete class at runtime. You cannot narrow an individual callback's parameter type to the concrete class it actually expects (tuple[type[SpecificClass], ...]) — that breaks structural assignability against the wider Protocol the dispatcher requires (parameter types are contravariant; a callback that only accepts a narrower type can't stand in for one the dispatcher will call with the wider type).
Fix at the call site inside the function body instead of the signature: cast("tuple[type[SpecificClass], ...]", classes), or cls = cast("type[SpecificClass]", classes[0]). Keep the public signature honestly wide.
6. **heterogeneous_dict splats can't be validated against multi-parameter constructors
fields = {"type_": DataType.int_, "name": "Integer"}
SimpleField(**fields)
Both checkers infer fields: dict[str, DataType | str] and then check every keyword argument against that whole union, rather than against each parameter's own specific type — because plain dict[str, ...] splatting isn't a TypedDict, so there's no per-key type information available. This isn't a narrowing bug or an inherent-bad-input case; it's a structural limitation of **dict splatting itself. Either convert the dict to a TypedDict (real fix, more invasive) or accept a targeted ignore comment — don't spend time trying to "fix" it any other way.
7. Community stub packages can behave differently per checker
If you add a community-maintained stub package (lxml-stubs, etc.) rather than relying on inline py.typed types, expect it to have its own bugs, and expect those bugs to manifest differently per checker. In this migration, lxml-stubs declares several attributes using the legacy stub syntax tag = ... # type: str (pre-PEP 526). ty tolerates this by falling back to Unknown for that attribute (safe, just loses precision). pyrefly mis-parses it as the attribute's type being the literal value Ellipsis, then reports every subsequent use (.strip(), .split(), slicing) as an error on a nonexistent EllipsisType method — a wave of dozens of false positives that has nothing to do with your code.
There is no fix on your side other than working around the stub bug per-checker:
[tool.pyrefly]
# Force pyrefly to treat the whole (broken-for-pyrefly) stub package as Any,
# while `ty` still gets full value from the same installed stub package.
replace_imports_with_any = ["lxml.*"]
Don't assume "we added the stub package" is the end of the story — verify both tools independently after adding any third-party stub dependency, because "more type information" is not always strictly better across tools.
8. Fixing the root cause makes old workaround cast()s redundant — clean them up
Once you fix the systemic issue, re-run both tools and look specifically for redundant-cast warnings. Every cast("Element", root) that existed purely to placate mypy's ignore_missing_imports fallback becomes genuinely unnecessary once the real type flows through correctly, and leaving it in is now dead weight (and a ty/pyrefly warning) rather than a workaround. This is a good automatic signal that the root-cause fix actually landed.
9. Some "strict" rules are mechanical churn, not bug-catching — decide explicitly, don't default to on
Pyrefly's strict preset (and to a lesser extent ty's optional rules) includes checks like missing-override-decorator (PEP 698 @override), which can flag dozens to low-hundreds of ordinary __repr__/__init__/method overrides in any codebase with meaningful inheritance. This is a legitimate check with real value in large team codebases (catches silently-broken overrides after a base-class rename), but adding @override everywhere is a large, purely mechanical diff disconnected from the actual "fix type errors" task.
Don't silently turn this on or off. Surface it explicitly (to a human reviewer, or via an explicit question if you're an agent) before deciding: disable it with a documented reason, or actually do the sweep. Either is defensible; picking silently isn't.
10. Test-file "narrowing noise" is real but should not become a license to blanket-suppress everything
The overwhelming majority of test-file errors in a mature test suite will be the same shape: construct an object, then immediately access/assign a field typed X | None or A | B | None, without a narrowing assert x is not None — because the test knows the value is set (it just set it three lines up) but the checker doesn't. This is legitimately safe to bulk-suppress scoped to the test tree:
[[tool.pyrefly.sub-config]]
matches = "tests/**/*"
[tool.pyrefly.sub-config.errors]
missing-attribute = "ignore"
[[tool.ty.overrides]]
include = ["tests/**"]
rules = { unresolved-attribute = "ignore", invalid-assignment = "ignore" }
But scope the suppressed rule names narrowly (unresolved-attribute/missing-attribute/the specific invalid-assignment shape), not broad categories like invalid-argument-type/bad-argument-type — those catch genuinely wrong types passed into calls, which do happen in test code (typos, copy-paste of the wrong fixture) and are worth keeping visible. In the fastkml migration, roughly 90% of test-file errors were narrowing noise safely bulk-suppressed, and the remaining 10% surfaced one genuine test bug (a string literal assigned where an enum member was expected) plus a batch of deliberately-invalid-input tests that just needed their ignore-comment syntax migrated (see pitfall 1).
Phase 3 — Work file by file for what's left
After the systemic fix and the bulk test-suppression, what remains is usually a short, tractable list (tens, not hundreds, of diagnostics). For each:
-
Read the surrounding code before deciding how to fix it. The same
unresolved-attributeshape can be a real narrowing gap (addassert x is not None, matching the style already used elsewhere in the file), a genuine latent bug (an off-by-one/empty-tuple case an unpacking*argscall didn't guard against), or a structural dispatch limitation (pitfall 5). -
Prefer a real fix over a cast or ignore wherever one exists cheaply: correcting a wrong return-type annotation (
Optional[X]that never actually returnsNone), addingisinstancenarrowing instead of a loose dict-dispatch, genericizing afind/find_all-style utility with@overload+ aTypeVarinstead of returningobject. -
Use
cast()with a one-line comment explaining *why* when the limitation is structural (pitfalls 4-6), not because you're in a hurry. - Re-run the checker on just that file after each fix (
ty check path/to/file.py) — faster feedback loop than re-running the whole tree, and it confirms the fix didn't introduce a new diagnostic in the same file.
Phase 4 — Tighten to "maximum quality"
Once both tools report zero errors on the fixed baseline, raise the bar deliberately rather than assuming the default preset is already strict:
[tool.pyrefly]
preset = "strict" # not "legacy" / "default" — legacy exists specifically to ease mypy migrations, it's a floor, not a target
[tool.ty.rules]
# Promote rules ty ships at warn/ignore by default; discover the full list with `ty explain rule`.
possibly-missing-attribute = "error"
possibly-missing-import = "error"
possibly-unresolved-reference = "error"
missing-type-argument = "error"
unused-ignore-comment = "error"
unused-type-ignore-comment = "error"
redundant-cast = "error"
Discover what's available rather than guessing:
ty explain rule # every rule, default level, rationale, examples
pyrefly check --help # --preset options, --error/--warn/--ignore, --replace-imports-with-any, etc.
pyrefly dump-config # what's actually active for this project right now
Phase 5 — Verify
Don't call it done on "the type checker is quiet." Run the full loop:
ty check <src> <tests>
pyrefly check <src> <tests>
ruff check --no-fix <src> <tests> # type-fix edits (casts, isinstance, overloads) can introduce lint issues
ruff format --check <src> <tests>
python -m pytest # with optional runtime deps installed
uv pip uninstall <optional-dep> # e.g. lxml
python -m pytest -m "not <slow-marker>" # confirm the fallback code path still works
uv pip install -e ".[typing,<optional-dep>]"
python3 -c "import tomllib; tomllib.load(open('pyproject.toml','rb'))"
The "uninstall the optional dependency and re-run tests" step matters specifically because Phase 2's fix changes how the optional backend is typed, not just how it's imported — if the runtime fallback logic was touched at all while chasing type errors, this is the step that catches a broken fallback path before it ships.
Case study numbers (fastkml)
For calibration on what "a lot of noise, mostly one root cause" looks like in practice:
- Baseline:
tyreported 237 diagnostics across source + tests;pyreflyreported 40 errors (with an already-too-permissive config suppressing 38 more). - One import-site fix (Phase 2, aliasing the duck-typed
ElementProtocol andetreemodule tolxml's real stubs underTYPE_CHECKING) collapsedty's source-only count from 47 to 13 in a single step. - Total genuine bugs found and fixed in library source: 5 (a missed
.getroot()call, an unguarded empty-tuple unpack in azip_longestloop, a too-loosetype[object]dispatch signature, aSelf-in-a-list invariance issue, and one example script passingbyteswherestrwas expected). - Total genuine bugs found and fixed in tests: 1 (a string literal compared against an enum field).
- Final state: zero errors on
ty checkandpyrefly checkfor the CI-covered scope, with all pre-existing tests still passing, both with and without the optionallxmlbackend installed.
If you made it this far, you might be interested in the cost:
- Total cost: $40.44
- Total duration (API): 58m 29s
- Total duration (wall): 1h 48m 15s
- Total code changes: 635 lines added, 224 lines removed
Usage by model:
- claude-haiku-4-5: 1.1k input, 39 output, 0 cache read, 0 cache write ($0.0013)
- claude-sonnet-5: 23.1k input, 227.0k output, 106.7m cache read, 993.5k cache write ($40.44)
Sebastian Pölsterl
scikit-survival 0.28.0 released
I am pleased to announce the release of scikit-survival 0.28.0.
A highlight of this release is the support for Polars DataFrames alongside pandas DataFrames via the Narwhals dataframe abstraction layer. In addition, this release adds support for scikit-learn 1.9.
Support for Polars
Polars is a data frame library similar to pandas, but with its core written in Rust instead of Python, which often gives Polars an advantage in terms of performance.
All datasets shipped with scikit-survival can now be loaded as a Polars DataFrame by specifying the
output_type argument.
from sksurv.datasets import load_gbsg2
# return X as a polars DataFrame
X, y = load_gbsg2(output_type="polars")
X.head()
shape: (5, 8)
┌──────┬────────┬───────┬──────────┬────────┬─────────┬────────┬───────┐
│ age ┆ estrec ┆ horTh ┆ menostat ┆ pnodes ┆ progrec ┆ tgrade ┆ tsize │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ enum ┆ enum ┆ f64 ┆ f64 ┆ enum ┆ f64 │
╞══════╪════════╪═══════╪══════════╪════════╪═════════╪════════╪═══════╡
│ 70.0 ┆ 66.0 ┆ no ┆ Post ┆ 3.0 ┆ 48.0 ┆ II ┆ 21.0 │
│ 56.0 ┆ 77.0 ┆ yes ┆ Post ┆ 7.0 ┆ 61.0 ┆ II ┆ 12.0 │
│ 58.0 ┆ 271.0 ┆ yes ┆ Post ┆ 9.0 ┆ 52.0 ┆ II ┆ 35.0 │
│ 59.0 ┆ 29.0 ┆ yes ┆ Post ┆ 4.0 ┆ 60.0 ┆ II ┆ 17.0 │
│ 73.0 ┆ 65.0 ┆ no ┆ Post ┆ 1.0 ┆ 26.0 ┆ II ┆ 35.0 │
└──────┴────────┴───────┴──────────┴────────┴─────────┴────────┴───────┘
Transforming DataFrames
scikit-learn enables transformers to return Polars data frames via the set_output API.
from sklearn import set_config
from sklearn.preprocessing import StandardScaler
set_config(transform_output="polars")
# standarize the columns of a polars DataFrame
X_standarized = StandardScaler().fit_transform(
X.select("age", "tsize")
)
scikit-surival has two transformers that now accept polars and pandas data frames as input: ClinicalKernelTransform, and OneHotEncoder.
ClinicalKernelTransform
ClinicalKernelTransform computes a kernel matrix, so the output will be a numpy array, as before.
With scikit-survival 0.28.0, it is aware of the
polars colum types
String, Categorical, Enum and Object.
However, polars does not have a concept similar to ordered categories in pandas: pd.Categorical([…], ordered=True).
When computing the clinical kernel for a polars data frame, you can specify the order
of categories with the ordinal_categories argument, otherwise all non-numeric columns
will be treated as nominal columns, where values have no specific order
(e.g. the column horTh with values “yes” and “no” in the example below).
from sksurv.kernels import ClinicalKernelTransform
K = ClinicalKernelTransform(
ordinal_categories={"tgrade": ["I", "II", "III"]},
).fit_transform(
X.select("age", "horTh", "tgrade")
)
OneHotEncoder
OneHotEncoder encodes the string-type columns to numeric columns.
It automatically returns a data frame of the same type as the input:
from sksurv.preprocessing import OneHotEncoder
X_onehot = OneHotEncoder().fit_transform(X.select("horTh", "tgrade"))
X_onehot.head()
shape: (5, 3)
┌───────────┬───────────┬────────────┐
│ horTh=yes ┆ tgrade=II ┆ tgrade=III │
│ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ f64 │
╞═══════════╪═══════════╪════════════╡
│ 0.0 ┆ 1.0 ┆ 0.0 │
│ 1.0 ┆ 1.0 ┆ 0.0 │
│ 1.0 ┆ 1.0 ┆ 0.0 │
│ 1.0 ┆ 1.0 ┆ 0.0 │
│ 0.0 ┆ 1.0 ┆ 0.0 │
└───────────┴───────────┴────────────┘
Fitting survival models
Finally, you can pass a polars data frame to the fit and predict functions of any estimator.
from sklearn.pipeline import make_pipeline
from sksurv.linear_model import CoxPHSurvivalAnalysis
pipe = make_pipeline(
OneHotEncoder(), StandardScaler(), CoxPHSurvivalAnalysis()
)
pipe.fit(X[:500], y[:500])
risk_scores = pipe.predict(X[500:])
Note that this only works for eager data frames, lazy data frames will give an error.
X_lazy = X.lazy()
pipe.fit(X_lazy[:500], y[:500])
Traceback (most recent call last):
File "example.py", line 21, in <module>
pipe.fit(X_lazy[:500], y[:500])
File "…/site-packages/sklearn/base.py", line 1403, in wrapper
return fit_method(estimator, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
…
File "…/site-packages/sksurv/_dataframe/_input.py", line 96, in ensure_eager_dataframe
_reject_polars_lazyframe(obj)
File "…/site-packages/sksurv/_dataframe/_input.py", line 84, in _reject_polars_lazyframe
raise TypeError(_LAZYFRAME_NOT_SUPPORTED_MSG)
TypeError: polars.LazyFrame is not supported; call .collect() before passing to scikit-survival.
New Contributors
A big shoutout to our two new contributors:
- cakedev0 for adding support for scikit-learn 1.9
- 55Kamiryo for adding support for Polars data frames.
Updated Dependencies
With this release, the minimum supported version are:
| Package | Minimum Version |
|---|---|
| narwhals | 2.0.1 |
| scikit-learn | 1.9.0 |
Install
scikit-survival is available for Linux, macOS, and Windows and can be installed either
via pip:
pip install scikit-survival
or via conda
conda install -c conda-forge scikit-survival
Core Dispatch
Core Dispatch #7
Welcome back to Core Dispatch! This edition covers June 19 through July 5, 2026. Python 3.15.0 beta 3 landed on June 23, and beta 4 is up next on July 18, with 3.13.15 and 3.14.7 following on August 4. Last edition, we covered the Steering Council's request that the experimental JIT chart its futur
July 04, 2026
PyCon Ireland
Update on PyCon Ireland 2026: Venue Change and Extended CFP Deadline
Trinity College Dublin can no longer host PyCon Ireland 2026. We are confirming a new venue and date, and the Call for Proposals deadline has been extended to 30 August 2026.
Bob Belderbos
One Core, Two Interfaces, No Rewrites
When building applications, I always build the core first, then the interfaces. It was no different with Ask the Canon: a uv run main.py ask "..." CLI for quick iteration and validation, then the web app for MVP. Search, ranking, citations, all using the same engine.
