Case 163: Python Keyword Argument Renamed (Stub-Only API Break)¶
| Field | Value |
|---|---|
| Verdict | ๐ API_BREAK |
| Category | API Break |
| Platforms | Linux, macOS, Windows |
| Flags | API break |
Detected ChangeKinds |
python_api_parameter_renamed |
| Source files | examples/case163_python_kwarg_renamed/ |
Category: Python API | Verdict: ๐ API_BREAK
Verdict and consumer impact¶
mymod's compiled extension (mymod.abi3.so) is byte-identical between v1
and v2 โ same PyInit_mymod export, same imported Py* surface, same
abi3 tag. But the module's Python-visible contract, carried only in its
.pyi type stub, renames the keyword-only argument encoding โ codec
and drops its default. Every caller that wrote
transform(data, encoding="utf-8") now hits an unexpected-keyword
TypeError on v2 โ a real break for consumers that a tool comparing only
the compiled binary would score compatible.
Old/new diff¶
| v1.pyi | v2.pyi |
|---|---|
def transform(data, *, encoding: str = "utf-8") -> bytes: ... |
def transform(data, codec: str) -> bytes: ... |
abicheck command¶
This case ships only the .pyi type stubs โ no compiled .so โ so there is
no binary to hand to abicheck compare directly. abicheck dump recovers a
Python API surface the same way, from a .pyi sitting next to a real
extension binary; here we build that same snapshot shape from the stub
files alone, with abicheck's own stub-parsing entry point, then run the
real comparison against the resulting snapshots:
python3 - <<'PY'
from pathlib import Path
from abicheck.model import AbiSnapshot
from abicheck.python_api import surface_from_stub_file
from abicheck.serialization import save_snapshot
case = Path("examples/case163_python_kwarg_renamed")
for side, version in [("v1", "1.0"), ("v2", "2.0")]:
snap = AbiSnapshot(library="mymod.abi3.so", version=version)
snap.python_api = surface_from_stub_file(case / f"{side}.pyi", module_name="mymod")
save_snapshot(snap, f"{side}.abi.json")
PY
abicheck compare v1.abi.json v2.abi.json
Expected abicheck finding¶
Verdict: API_BREAK (exit 2)
- python_api_parameter_renamed: Python parameter renamed in transform: encoding -> codec
> Callers that passed it by keyword hit an unexpected-keyword TypeError.
The compiled binary is byte-identical -- this is the canonical break
the native-ABI check misses.
- python_api_default_removed: Python parameter default removed in transform: codec
> A parameter lost its default value, making a previously optional
argument mandatory; callers relying on the default now raise a
missing-argument TypeError.
Minimum evidence¶
min_evidence: L2 โ the compiled binary carries no trace of parameter
names or defaults for a plain CPython entry point (PyInit_mymod plus a
handful of generic Py* calls); the .pyi type stub is the only artifact
that names encoding/codec and their defaults at all, so it is the floor
for detecting this class of break.
Why abicheck catches it¶
abicheck's Python-API surface recovery statically parses the .pyi stub
with ast (never imports or executes the module) and diffs the resulting
function/parameter lists directly โ independent of, and blind to, whatever
the compiled .so's export table says.
Real-world deployment scenario¶
This is what a release pipeline for a compiled Python extension sees when
it ships a wheel and a separate .pyi stub (the common pybind11/nanobind/
Cython/stubgen pattern): the extension binary can be rebuilt untouched
across a release while the maintained stub silently drifts. A native-ABI
gate on the .so alone stays green; only a check that also reads the
shipped stub โ as abicheck does here โ catches the keyword rename before
downstream code that calls transform(data, encoding=...) starts failing.
Safe redesign¶
Keep the parameter name and its default (add the new spelling as an alias,
or accept **kwargs and migrate with a deprecation) so existing keyword
callers keep working โ a renamed public keyword argument, even in a
Python-only type stub, is a source-breaking change.
Cross-tool comparison¶
abidiff/abi-compliance-checker only compare the compiled ELF/PE/Mach-O
interface (symbols, DWARF types); neither has a mode that reads a .pyi
stub, so there is no meaningful reproduction of this finding with either
tool โ the compiled mymod.abi3.so genuinely is unchanged, which is
exactly the blind spot this case demonstrates.
Source files¶
v1.pyiv2.pyi
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.