Skip to content

Public type contracts

Async Batcher 0.3.0 includes a PEP 561 marker in its wheel and source distribution. Type checkers can read the library's inline annotations after a normal package installation. Use the version selector to match these contracts to your installed release.

Inference and explicit types

AsyncBatcher[T, S] accepts inputs of type T and returns S from process(). Its bounded map() iterator yields the same result type. from_callable() infers these parameters from an annotated synchronous or asynchronous processor:

from typing import assert_type

from async_batcher import AsyncBatcher


async def render(items: list[int]) -> list[str]:
    return [str(item) for item in items]


batcher = AsyncBatcher.from_callable(
    render,
    max_batch_size=32,
    max_batch_cost=100,
    item_cost=lambda item: 1,
)
assert_type(batcher, AsyncBatcher[int, str])


async def use_batcher() -> None:
    async with batcher:
        assert_type(await batcher.process(1), str)
        async for result in batcher.map([1, 2, 3], max_pending=8):
            assert_type(result, str)

A processor returning only None produces AsyncBatcher[T, None]. A processor that may return either results or None produces an optional result type. Per-item Exception and asyncio.CancelledError values are raised for their callers, so they are not successful result types. Annotate a processor's return type to express those alternatives accurately.

Processors can return ordered, sized iterables, including custom containers with __len__ and __iter__. Runtime checks still reject unordered containers, mappings, text, and a result count that differs from the batch size. Type checking does not replace those checks or the runtime validation of positive limits and finite costs. Ordinary integer and float costs, as well as numbers.Real implementations such as Fraction, are accepted.

The result parameter is covariant: a batcher returning str can be used where one returning object is required. Inputs remain invariant. GroupedBatcher[K, T, S] carries the group-key, input, and result types separately. BatcherStats and BatchEvent are immutable typed snapshots; monitoring callbacks must be synchronous and return None.

Checking changes

The dedicated typecheck group pins mypy independently of optional SDK stubs in the typing group. Run the same installed-package check as CI:

uv sync --locked --no-default-groups --group typecheck
uv run --no-sync python .github/scripts/check_public_typing.py

The helper builds and installs a wheel in an isolated temporary environment, then checks strict consumer examples outside the checkout. Positive examples assert inferred types. Negative examples require exact error locations and codes for invalid inputs, incompatible callbacks, unknown options, and mutation of immutable snapshots. Removing the installed py.typed marker must also make the positive example fail. Distribution checks independently verify the marker in the direct wheel and the wheel rebuilt from the source distribution.

The core gate covers public callable, streaming, grouping, and observability APIs. Kafka and Kinesis have additional installed-wheel consumer checks, run in their adapter CI jobs:

uv run --no-sync python .github/scripts/check_public_typing.py --adapter kafka
uv run --no-sync python .github/scripts/check_public_typing.py --adapter aws

After syncing the typecheck group above, these commands export the selected extra from uv.lock and install its exact, hash-checked dependencies into a separate temporary environment. The AWS check also installs the existing locked typing stub group. The main environment and core-only CI jobs remain free of optional SDKs.

These checks verify record inputs, core keyword options, callback types, mapped results, and SDK metadata. Kafka results are RecordMetadata | None, preserving the producer's acks=0 behavior. The gate reads aiokafka's inline annotations using mypy's narrowly scoped follow_untyped_imports option because the locked SDK does not ship a py.typed marker; metadata field assertions prevent an unnoticed fallback to Any. Kinesis acknowledgements keep the existing dict[str, Any] contract for service response fields. Unknown keywords and incompatible input or result types must produce exact diagnostics; no missing-import errors are suppressed.

This does not claim strict checking of every private implementation or other optional SDK adapter. The checker remains a development dependency; installing the core library adds no runtime dependencies.