DEV Community

Dakota Huang
Dakota Huang

Posted on

Make a JSONL of Returns and Raises Before One Extract

Pin public returns and exception types before any extract. Treat that JSONL file as the merge gate. One private helper remains the only allowed change.

Why layout diffs miss contract breaks

Messy repos fail extracts inside return and raise paths. The extracted helper often looks cleaner during review. Callers can still receive a different exception class.

Git still shows a tidy looking split. Tests that assert only HTTP status stay green. The real contract moved without a failing check.

Public callables keep informal promises in exception types. They also keep promises in returned keys. A rename of a private line does not prove those promises held.

What one fingerprint must store

Each public call writes one JSONL row. The row stays boring on purpose.

  1. Store the fully qualified callable name.
  2. Store digests of args and kwargs.
  3. Store the outcome flag: return or raise.
  4. Store the type name for value or exception.
  5. Store a digest of a stable representation.

Drop wall-clock timestamps from every row. Drop object ids from every row. Drop memory addresses from every row.

Those fields churn without a contract change. A sixteen character SHA-256 prefix is enough here. Longer prefixes add little for local fixtures.

Do not pin traceback text in this workflow. Frames embed paths and line numbers. That noise collides with layout edits you want to allow.

Worked example: a messy loader

The next module is a labeled proposal. It is not production code. Parsing, defaults, and errors share one function.

# messy_loader.py — illustrative module under test
from __future__ import annotations

import json
from pathlib import Path
from typing import Any


class LoadError(RuntimeError):
    """Caller-facing load failure."""


def load_record(path: str, *, strict: bool = True) -> dict[str, Any]:
    raw = Path(path).read_text(encoding="utf-8")
    if not raw.strip():
        if strict:
            raise LoadError("empty record")
        return {"id": "missing", "ok": False}
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise LoadError("invalid json") from exc
    if not isinstance(data, dict):
        raise LoadError("record must be an object")
    key = data.get("id")
    if key is None:
        key = Path(path).stem
    else:
        key = str(key).strip().lower()
    data["id"] = key
    data.setdefault("ok", True)
    return data
Enter fullscreen mode Exit fullscreen mode

The id normalization is the extract target. Leave that logic in place first. Pins must exist before the split.

Fixture set for returns and raises

Create five synthetic files under fixtures/. Keep every payload tiny and local.

  1. ok.json holds a normal object with id.
  2. empty.json is whitespace only after strip.
  3. list.json is a JSON array, not object.
  4. bad.json is not valid JSON text.
  5. no_id.json is an object without id.

Empty plus strict=True must raise LoadError. Empty plus strict=False must return a placeholder. That pair is the interesting branch.

Example fixture bodies follow. Do not use live customer data.

{"id": "Abc-1", "ok": true}
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode
[]
Enter fullscreen mode Exit fullscreen mode
{not-json
Enter fullscreen mode Exit fullscreen mode
{"name": "anon"}
Enter fullscreen mode Exit fullscreen mode

Six recorded calls cover those files. The extra call is empty.json with strict=False. Returns and raises both need rows.

Recorder: write the JSONL pins

This harness is a worked example. Point it only at fixtures you own. Never hash secrets or credentials.

# pin_callables.py — worked example
from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any, Callable

from messy_loader import load_record


def _digest(value: Any) -> str:
    text = json.dumps(value, sort_keys=True, default=str, ensure_ascii=True)
    return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]


def pin_call(
    rows: list[dict[str, Any]],
    fn: Callable[..., Any],
    *args: Any,
    **kwargs: Any,
) -> None:
    row: dict[str, Any] = {
        "qualname": f"{fn.__module__}.{fn.__qualname__}",
        "args": _digest(list(args)),
        "kwargs": _digest(kwargs),
    }
    try:
        result = fn(*args, **kwargs)
    except Exception as exc:  # pin the public surface, including unexpected types
        row["outcome"] = "raise"
        row["type"] = f"{type(exc).__module__}.{type(exc).__name__}"
        row["value"] = _digest(str(exc))
    else:
        row["outcome"] = "return"
        row["type"] = type(result).__name__
        row["value"] = _digest(result)
    rows.append(row)


CASES = [
    ("fixtures/ok.json", {"strict": True}),
    ("fixtures/empty.json", {"strict": True}),
    ("fixtures/empty.json", {"strict": False}),
    ("fixtures/list.json", {"strict": True}),
    ("fixtures/bad.json", {"strict": True}),
    ("fixtures/no_id.json", {"strict": True}),
]


def record_rows() -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for path, kwargs in CASES:
        pin_call(rows, load_record, path, **kwargs)
    return rows


def main() -> None:
    rows = record_rows()
    out = Path("callable_pins.jsonl")
    payload = "\n".join(json.dumps(r, sort_keys=True) for r in rows) + "\n"
    out.write_text(payload, encoding="utf-8")
    print(f"wrote {len(rows)} pins to {out}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Commit callable_pins.jsonl with those fixtures. That file is the public contract. Reviews should read the JSONL before the helper diff.

A sample row looks like this. Digests will differ on your disk.

{"args": "a1b2c3d4e5f60817", "kwargs": "90ab12cd34ef5678", "outcome": "raise", "qualname": "messy_loader.load_record", "type": "messy_loader.LoadError", "value": "0123abcd4567ef89"}
Enter fullscreen mode Exit fullscreen mode

The type column is the exception contract. The value column is the message contract. Changing either is a public break.

Replay: pytest must match every row

The replay test is the merge gate. It does not score naming style.

# test_callable_pins.py — worked example
from __future__ import annotations

import json
from pathlib import Path

from pin_callables import record_rows


def test_public_callable_fingerprints_hold():
    text = Path("callable_pins.jsonl").read_text(encoding="utf-8")
    expected = [json.loads(line) for line in text.splitlines() if line.strip()]
    actual = record_rows()
    assert actual == expected
Enter fullscreen mode Exit fullscreen mode

Share CASES between recorder and replay. Duplicated case lists drift. Drift produces false greens after a missed fixture.

Run the pin test in a clean tree.

python pin_callables.py
python -m pytest test_callable_pins.py -q
Enter fullscreen mode Exit fullscreen mode

Green pins mean the public surface is frozen. Only then is an extract allowed.

Prove the gate fails on purpose

Do not trust a test you have not seen fail. Break one fixture return on purpose. Run pytest again and require red.

A cheap break is enough. Edit ok.json and change "Abc-1" to "Zzz-9". The return digest must move. Restore the fixture before you continue.

A gate that cannot fail is decoration. Decoration does not protect callers. Two minutes here saves a bad extract.

Numbered extract workflow

Follow this order without skipping steps.

  1. List public callables in the messy module.
  2. Build fixtures that hit return and raise paths.
  3. Record JSONL fingerprints with the harness above.
  4. Commit fixtures, JSONL, and replay test together.
  5. Prove replay fails after one intentional return edit.
  6. Extract one private helper. Keep public signatures stable.
  7. Re-run replay and require an exact JSONL match.
  8. Reject extra public renames inside the same diff.

Step five is the sanity check. It is not optional ceremony. Skipping it ships false greens.

Public list size stays small on purpose. One module. One callable. Six rows. Broader sweeps hide the first break.

The smallest safe change

Only _normalize_id should move in this diff. Public load_record must keep its signature.

def _normalize_id(data: dict, path: str) -> str:
    key = data.get("id")
    if key is None:
        return Path(path).stem
    return str(key).strip().lower()
Enter fullscreen mode Exit fullscreen mode

Call it from load_record and stop. Do not edit LoadError text. Do not swap the strict default.

Do not add logging calls in the same patch. Do not reorder setdefault relative to id. Do not widen the returned dict.

If JSONL bytes change, the extract is too large. Revert the helper and split less. Public type names are part of the pin.

LoadError must remain messy_loader.LoadError. A move to errors.LoadError is a second change. Record new pins only after that dedicated diff.

Decision table before merge

Use this table before you merge.

Observation Action
Replay green, diff is one helper Merge the extract
Replay green, diff also renames public names Split the diff
Replay red, type changed Stop. Restore the exception class
Replay red, value changed Stop. Restore messages and returns
Replay red, qualname changed Stop. Public callable moved
Recorder wants network or credentials Abort. Pins are local fixtures only

Value digest changes include message text. Exception messages are caller contract here. Do not clean wording in the same diff.

kwargs digest catches default flips. strict=True becoming implicit still hashes. That is a public change. Keep it out.

Where a free coding model can sit

A model may propose the helper body. It must not edit pin files.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode offers free model access and a free server option. That pair can draft a local extract proposal. Paste the messy function and the pin schema. Ask for one private helper only.

Keep fixture bodies off the server when they hold secrets. Synthetic ids in this example are fine. Production dumps are not fine.

The merge rule does not change after that draft. Replay must match row for row. Model prose is not evidence of safety.

Limitations

json.dumps(..., default=str) is a stability choice. It is not a full serializer. datetime values can still churn across runs.

Sets need an explicit sorted conversion first. Unsorted sets will flicker the digest. Floats need a rounding rule you define.

Return hashes ignore most side effects. Disk writes can still change under you. Log lines can still change under you.

Do not pretend a return hash covers I/O. Pin I/O with a different harness. This article does not provide that harness.

A later public rename will fail qualname. Update JSONL only for an intended public move. That update is a second change. It does not belong in the extract.

str(exc) drops chained cause text. __cause__ can still change under you. Add a cause digest only when callers read it.

Who should not use this

Skip this when contract tests already cover each callable. Skip this when module import talks to the network. Skip this when returns embed raw secrets.

Skip this when the function is non-deterministic by design. Random ids and clocks need a different freeze. Hashing them will flap every run.

Do not fingerprint production traffic at all. Do not hash live customer payloads. Fixtures stay in-repo and fully synthetic.

Skip this for binary blobs without a defined digest. default=str on bytes is a false contract. Define an explicit hex digest first.

What this workflow does not claim

It does not claim faster reviews in all teams. It does not claim a model replaces pytest. It does not claim free servers are private by default.

It claims one checkable rule. Frozen return and raise pins make a one-helper extract reviewable.

Public fingerprints first. One helper second. Keep the JSONL in git.

If you already use a free coding server, keep that server off the pin file.

Top comments (0)