Skip to content

build: upgrade mypy to 2.3.0 - #68

Merged
dariero merged 2 commits into
mainfrom
codex/bump-mypy
Jul 27, 2026
Merged

build: upgrade mypy to 2.3.0#68
dariero merged 2 commits into
mainfrom
codex/bump-mypy

Conversation

@dariero

@dariero dariero commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Upgrades mypy from 2.2.0 to 2.3.0. This one needs an abstract-requirement edit: the existing <2.3 ceiling excludes 2.3.0, so a lock refresh alone cannot reach it.

Version delta

Package Before After Delta Constraint
mypy 2.2.0 2.3.0 minor, above the previous ceiling >=2.2.0,<2.3 to >=2.3.0,<2.4 (pyproject.toml:21)

2.3.0 is the only stable release between 2.2.0 and latest, and 2.3.0 is latest. The new ceiling keeps the repository's existing convention of a one-minor window for mypy.

$ git diff pyproject.toml
-    "mypy>=2.2.0,<2.3",
+    "mypy>=2.3.0,<2.4",

Lock regenerated with the second form of .agents/skills/upgrade-dependencies/SKILL.md step 5 (retain existing preferences, resolve only required changes), not --upgrade:

$ uvx --from uv==0.11.30 uv lock --python 3.14 --prerelease disallow
Resolved 56 packages in 113ms
Updated mypy v2.2.0 -> v2.3.0

No transitive churn. 2.3.0 raises its own floor from librt>=0.12.0 to librt>=0.13.0, and librt was already locked at exactly 0.13.0, so nothing else had to move:

added:   []
removed: []
moved:   {'mypy': ('2.2.0', '2.3.0')}
total packages: 56 -> 56

Breaking changes, and which touch this repository

python/mypy publishes no GitHub Releases; CHANGELOG.md at tag v2.3.0 is the authoritative note. Upstream's standing policy, quoted from that file:

Mypy doesn't use SemVer, since most minor releases have at least minor backward incompatible changes in typeshed, at the very least. Also, many type checking features find new legitimate issues in code. These are not considered backward incompatible changes, unless the number of new errors is very high.

So 2.3.0 is a feature release that upstream expects may legitimately produce new errors. The full change set was read and classified.

Exactly one change in the release can add errors. Every other type-checking change either removes a false positive or removes narrowing.

Change Direction Touches this repository
PR 21675, dataclass narrowing regression for Python >= 3.13 Fixes a false negative, so previously-unchecked blocks become checked Precondition present, trigger absent. See below.
PR 21694, __replace__ exempted in infer_variance Changes inferred variance Precondition present, trigger absent. See below.
PR 21706, custom __eq__ in membership narrowing Produces strictly less narrowing No. Needs warn_unreachable = True; --strict does not imply it and pyproject.toml does not set it.
PR 21709, frozendict membership narrowing Additive No. frozendict exists in builtins.pyi only under sys.version_info >= (3, 15); python_version = "3.14".
PR 21651, Coroutine for unannotated async def Can add errors No. Gated on func.type is None, which --disallow-untyped-defs already forbids. The one async def in scope (eval/ragaliq_adapter.py:29) is fully annotated.
PR 21668, error code on the unbound-TypeVar note Changes suppressibility No. No TypeVar in the checked scope, and no type: ignore[...] or disable_error_code for it to affect.
PR 21714, Emscripten detection via PYODIDE env var Changes inferred platform No, on darwin or ubuntu. Recorded as a new environment sensitivity.
12 typeshed stdlib stub files Can add errors No. See below.
mypyc: Final instance attributes read-only at runtime Runtime behaviour of compiled code No. Affects mypy's own wheels, not this repository's code.
mypyc free-threading memory-safety cost Performance No. This is a GIL-enabled 3.14.6 build.
Pre-announcement: --native-parser to become default Not yet in effect mypy/options.py:389 still reads self.native_parser = False. Watch item for 2.4.

On the two __replace__ fixes, the reason they are inert matters. "This repository has no dataclasses" would be the wrong dismissal. mypy/plugins/dataclasses.py:388-389 gates _add_dunder_replace on python_version >= (3, 13) alone, and with no plugins = ["pydantic.mypy"] configured the four pydantic models go through the generic dataclass_transform path. At python_version = "3.14" they do get a synthesized __replace__. They are inert because the triggers are absent:

  • PR 21675 needs intersect_instances to build an ad-hoc intersection of two such classes. Every isinstance/issubclass target in the checked scope is a builtin or ABC (str, int, bool, dict, list, tuple, Mapping, np.ndarray) - never a model class. And the cross-base check_compatibility path needs a class with two or more bases; there is none in scope.
  • PR 21694 needs a class with both inferred variance and a __replace__ member. The one PEP 695 generic, models.py:20 class _ImmutableMapping[Key, Value](Mapping[Key, Value]), has no __replace__; the four classes that do get one are non-generic. The two sets are disjoint.

On typeshed. Only 12 stdlib stubs changed, and pathlib.pyi, os/__init__.pyi and json/*.pyi are not among them - which covers every stdlib import in the checked scope. stubs/ changed only for librt, not for any third-party distribution; numpy, pydantic, openai and ragaliq all ship py.typed, so they are checked from inline source types no mypy release can alter. The largest typeshed change, typing_extensions.pyi (+70/-14, including TypedDict and TypeVarTuple becoming distinct symbols rather than re-exports at 3.14), is inert here for a reason verified by grep rather than assumed:

$ grep -n "typing_extensions\|TypedDict" config.py corpus.py models.py pipeline.py eval/ragaliq_adapter.py
pipeline.py:24:from typing import Any, Protocol, TypedDict, cast
corpus.py:9:from typing import TypedDict

TypedDict comes from typing, not typing_extensions, and typing_extensions is imported nowhere in the checked scope.

errorcodes.py is byte-identical between the tags and the --strict flag table is unchanged, so no new diagnostic can arrive under a newly-enabled code.

Prediction recorded before running anything

Two parts, both recorded before any lock was regenerated.

  1. The pin blocks the upgrade before mypy runs. With only the constraint relaxed and the locks untouched, CI fails at "Verify dependency lock consistency" - uv lock --check then the byte-exact cmp - both of which run before "Run all repository hooks". The locks must be regenerated in the same commit.
  2. mypy 2.3.0 then reports Success: no issues found in 5 source files. This was recorded as a structural claim, not a hope: every 2.3.0 behaviour change removes a false positive or removes narrowing except PR 21675, and that one's trigger shape does not exist in this repository. Coverage stays at 96.05%, all twelve hooks pass, the free suite is unaffected because mypy is not imported by any test, and no README text needs editing because lines 77, 394 and 430 mention mypy without a version number.

A dry run recorded before any file was edited, using uvx so the committed environment was untouched:

$ uvx --python 3.14 --from mypy==2.3.0 --with numpy==2.5.1 --with openai==2.45.0 \
    --with pydantic==2.13.4 --with ragaliq==0.2.0 --with pytest==9.1.1 \
    mypy config.py corpus.py models.py pipeline.py eval/ragaliq_adapter.py
Success: no issues found in 5 source files

What actually happened

Exactly the prediction.

Gate Before (main) After
uv lock --check Resolved 56 packages Resolved 56 packages
Frozen export vs pylock.toml identical identical
uv pip check 54 packages compatible 54 packages compatible
pytest eval/ -q 335 passed, 13 deselected 335 passed, 13 deselected
Coverage gate 96.05% 96.05%
mypy strict scope Success, 5 source files (2.2.0) Success, 5 source files (2.3.0)
pre-commit run --all-files 12 hooks passed 12 hooks passed
pre-commit validate-config exit 0 exit 0
ruff format --check . 10 files already formatted 10 files already formatted
ruff check . All checks passed All checks passed
agent-policy-symbols exit 0 exit 0
Default selection 13 deselected 13 deselected

Free validation command and result:

$ .venv/bin/python -m pytest -m "not openai and not rag_test" --cov --cov-report=term-missing eval/ -q
TOTAL                       642     18    218     16  96.05%
Required test coverage of 95.0% reached. Total coverage: 96.05%
335 passed, 13 deselected in 0.90s
$ .venv/bin/python -m mypy --version
mypy 2.3.0 (compiled: yes)

One incidental observation, recorded because it was surfaced by this work and because it is not a defect in this change. Running uv pip sync against a long-lived local .venv reported uv pip check finding "multiple installed distributions" for mypy. The cause was pre-existing pollution in that developer venv: mypy-2.2.0.dist-info/licenses/ contained LICENSE 3 files - macOS duplicate-copy names that no uv install writes and that therefore appear in no RECORD, so uninstall could not remove the directory. Recreating the venv with the documented uv venv --python 3.14 plus uv pip sync gives uv pip check exit 0, and the clean-clone run below confirms a fresh install is clean. .venv/ is gitignored; nothing entered the repository.

The mutation question

Which behaviours of this dependency does the suite exercise? The mypy hook and the documented mypy command exercise strict checking over exactly five files, named as explicit args with pass_filenames: false:

config.py  corpus.py  models.py  pipeline.py  eval/ragaliq_adapter.py

That is a genuine assertion surface - --strict bundles thirteen flags, and Success over those files is a real result. But its scope is exactly five files, and it is produced by a hook, not by a pytest node.

For each breaking change that touches a real call site, which test would have failed?

Change Touches the checked scope Test that would have failed
PR 21675 dataclass narrowing (the only error-adding change) Precondition yes, trigger no The mypy hook itself would have failed, and that is the honest answer here: a newly surfaced strict error fails pre-commit run --all-files and the CI "Run all repository hooks" step. This is the one upgrade in the audit where the existing gate genuinely would have caught the change.
PR 21694 variance inference Precondition yes, trigger no Same - the mypy hook.
warn_unused_ignores on a now-unnecessary type: ignore No such comment exists in scope Not applicable. Verified: zero type: ignore in the five checked files.
Typeshed typing_extensions changes No Not applicable.

The finding, stated plainly. The gap is not in what mypy checks, it is in what mypy is pointed at. Four tracked Python files are outside [tool.mypy] files and were never type-checked at 2.2.0 either: benchmark_retrieval.py, eval/check_agent_policy_symbols.py, eval/conftest.py, and eval/test_verdigrise.py. Nothing in this upgrade's green result says anything about them. Concretely, eval/test_verdigrise.py carries seven # type: ignore[index] comments (lines 1111, 1113, 1115, 1117, 1142, 1144, 1146) that sit permanently outside --warn-unused-ignores, so a mypy release that made any of them unnecessary could never be detected. That is a standing scope decision, not a regression introduced here, and it is deliberately not changed in this pull request - widening the type-checking scope during a version upgrade would confound the upgrade's own evidence.

Lock byte-comparison

$ uvx --from uv==0.11.30 uv lock --check --python 3.14 --prerelease disallow
Resolved 56 packages in 31ms
$ uvx --from uv==0.11.30 uv export --frozen --format pylock.toml --all-groups \
    --no-emit-project --python 3.14 --prerelease disallow --no-header --quiet \
    -o "$generated_dir/pylock.generated.toml"
$ cmp pylock.toml "$generated_dir/pylock.generated.toml"
cmp: IDENTICAL (exit 0)
$ shasum -a 256 pylock.toml "$generated_dir/pylock.generated.toml"
f0ffe3ac190d55a9742798302206d7f37819b0c5562fac2ee97eb69f32bb4f68  pylock.toml
f0ffe3ac190d55a9742798302206d7f37819b0c5562fac2ee97eb69f32bb4f68  .../pylock.generated.toml

pylock.toml invariants re-checked after the export:

requires-python = "==3.14.*"
sha256 count: 343
local paths / editable / file:// / git+ : 0
non-PyPI index entries: index = "https://pypi.org/simple"

Clean-clone transcript

Isolated temp directory, both provider key variables unset and confirmed absent by presence check only, no sibling ../RagaliQ reachable, README install commands verbatim with bare uv as documented.

=== provider key presence check (names only, never values) ===
OPENAI_API_KEY: absent
ANTHROPIC_API_KEY: absent
=== clone root: /private/tmp/verdigrise-cleanclone.TiINJi ===
=== sibling RagaliQ reachable from clone parent? ===
not reachable
cloned HEAD: 2d5b655bb1881af4f9d839fdd147496f11f56a01  branch: codex/bump-mypy
worktree clean: yes
sibling RagaliQ reachable from repo root? not reachable
uv 0.11.32 (Homebrew 2026-07-23 aarch64-apple-darwin)

$ uv venv --python 3.14
Using CPython 3.14.6 interpreter at: /opt/homebrew/opt/python@3.14/bin/python3.14
$ uv pip sync --preview-features pylock --require-hashes pylock.toml
Installed 54 packages in 161ms
$ uv pip check
Checked 54 packages in 1ms
All installed packages are compatible

$ .venv/bin/python -m pytest eval/ -q
335 passed, 13 deselected in 1.72s

=== clone-to-green wall time: 4s (uv cache WARM: 17G) ===

$ .venv/bin/python -m pytest --cov --cov-report=term-missing eval/ -q
TOTAL                       642     18    218     16  96.05%
Required test coverage of 95.0% reached. Total coverage: 96.05%
335 passed, 13 deselected in 1.19s

$ .venv/bin/ruff format --check .   -> 10 files already formatted (exit 0)
$ .venv/bin/ruff check .            -> All checks passed! (exit 0)
$ .venv/bin/python -m mypy config.py corpus.py models.py pipeline.py eval/ragaliq_adapter.py
Success: no issues found in 5 source files (exit 0)
$ .venv/bin/pre-commit validate-config   -> exit 0
$ .venv/bin/pre-commit run --all-files   -> 12 hooks Passed (exit 0)
$ .venv/bin/python -m pytest eval/ --collect-only -q | tail -1
335/348 tests collected (13 deselected) in 0.36s
$ .venv/bin/python -m eval.check_agent_policy_symbols   -> exit 0

ragaliq 0.2.0 from .../.venv/lib/python3.14/site-packages/ragaliq/__init__.py

The clone-to-green figure is a warm-cache number: the uv cache was already populated (17G), so 4s measures command execution, not first contact. A cold-cache clone would additionally download 54 wheels.

Hidden costs

  • The new ceiling <2.4 means 2.4.0 will again require a source edit rather than a lock refresh. That is the intended trade - a tight window makes each tool bump a reviewed event - and it is worth restating rather than discovering later.
  • 2.3.0 pre-announces that --native-parser will become the default. Testing that flag against this scope before 2.4 ships is the cheap way to de-risk that upgrade; it is out of scope here.
  • The Emscripten detection change means a PYODIDE environment variable would now alter mypy's inferred platform where it previously could not. Irrelevant on darwin and ubuntu, but it is a new sensitivity to the ambient environment.
  • No provider calls were made at any point in producing this change.

@dariero

dariero commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

@codex review for deterministic/RagaliQ ownership, dependency reproducibility, Python 3.14 compatibility, public-clone portability, paid-call safety, golden-fixture integrity, marker correctness, public API compatibility, and unintended behaviour changes

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 2d5b655bb1

鈩癸笍 About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 馃憤.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@dariero
dariero merged commit b3a5984 into main Jul 27, 2026
4 checks passed
@dariero
dariero deleted the codex/bump-mypy branch July 27, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant