Skip to content

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.pyi
  • v2.pyi

See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.