skip to navigation
skip to content

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.

July 10, 2026 03:13 PM UTC


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 10, 2026 05:10 AM UTC

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

July 09, 2026 05:05 PM UTC


Python Software Foundation

The PSF D&I Workgroup Are Starting Office Hours in July!

July 09, 2026 02:11 PM UTC

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:

For more details, read the original announcement on the Django blog.

July 08, 2026 07:31 PM UTC


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.

July 08, 2026 06:46 PM UTC


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):

In the Circle, the Diameter is to Circumference as 1 to 16/5 - 4/239 - 1/3 15/(53) - 4/(2393) + 1/5 16/(55) - 4/(2395) -, &c. = 3.1459, &c. = π. This Series..I receiv’d from the Excellent Analyst..Mr. John Machin; and by means thereof, Van Ceulen’s Number..may be Examin’d.

However, the OED’s first citation had a markup bug:

As previous image, but: 1 to Misplaced &, where Misplaced and the ampersand are red on yellow background

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!

As previous image, but the sequence is now 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).

July 08, 2026 04:40 PM UTC


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

July 08, 2026 03:48 PM UTC


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 08, 2026 06:00 AM UTC

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

July 07, 2026 08:44 PM UTC


PyCoder’s Weekly

Issue #742: Wagtail as Admin, Random Values, Code Quality, and More (2026-07-07)

July 07, 2026 07:30 PM UTC


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, […]

July 07, 2026 05:51 PM UTC


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 […]

July 07, 2026 04:00 PM UTC


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.

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

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-53877: Heap buffer over-read in GDALRaster

CVE-2026-53878: Header injection possibility since DomainNameValidator accepted newlines in input

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.

July 07, 2026 02:00 PM UTC


PyCon

Welcome, Kattni!

July 07, 2026 11:50 AM UTC


Python Bytes

#487 Minimum requirements

Topics include dust, Hermes Agent, and llm-coding-agent 0.1a0.

July 07, 2026 08:00 AM UTC


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.

July 07, 2026 06:50 AM UTC


Seth Michael Larson

Playing the “Second Quest” of Legend of Zelda: Four Swords Adventures

July 07, 2026 12:00 AM UTC

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:

  1. Text editor tool: this tool replaces the four tools you defined to read, write, replace, and insert, text in text files
  2. 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...

July 06, 2026 01:00 PM UTC


Seth Michael Larson

Mario Kart World and “seamless” media

July 06, 2026 12:00 AM UTC

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

  1. Get a raw error-count baseline for both tools before touching any source. Categorize by error kind, not by file.
  2. 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.
  3. 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).
  4. 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-type is not unresolved-attribute).
  5. 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 @override sweep).
  6. 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.

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:

  1. Read the surrounding code before deciding how to fix it. The same unresolved-attribute shape can be a real narrowing gap (add assert 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 *args call didn't guard against), or a structural dispatch limitation (pitfall 5).
  2. Prefer a real fix over a cast or ignore wherever one exists cheaply: correcting a wrong return-type annotation (Optional[X] that never actually returns None), adding isinstance narrowing instead of a loose dict-dispatch, genericizing a find/find_all-style utility with @overload + a TypeVar instead of returning object.
  3. Use cast() with a one-line comment explaining *why* when the limitation is structural (pitfalls 4-6), not because you're in a hurry.
  4. 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:

If you made it this far, you might be interested in the cost:

Usage by model:

July 05, 2026 04:29 PM UTC


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:

Updated Dependencies

With this release, the minimum supported version are:

PackageMinimum Version
narwhals2.0.1
scikit-learn1.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

July 05, 2026 04:22 PM UTC


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 05, 2026 12:00 AM UTC

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.

July 04, 2026 12:00 AM UTC


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.

July 04, 2026 12:00 AM UTC