{"record":{"id":"cb189b0f19cb6b99","repo":"roboflow/supervision","slug":"callback-must-return-list-detections-when-batc","errorCode":null,"errorMessage":"Callback must return `list[Detections]` when `batch_size > 1`. Got: {type(detections_in_slices)}","messagePattern":"Callback must return `list\\[Detections\\]` when `batch_size > 1`\\. Got: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/detection/tools/inference_slicer.py","lineNumber":586,"sourceCode":"        if _is_windowed_raster(image):\n            slices = []\n            for offset in offsets:\n                x_min, y_min, x_max, y_max = (int(v) for v in offset)\n                window = ((y_min, y_max), (x_min, x_max))\n                with self._raster_read_lock:\n                    bands = image.read(window=window)\n                slices.append(np.ascontiguousarray(np.transpose(bands, (1, 2, 0))))\n            resolution_wh = (image.width, image.height)\n        else:\n            slices = [crop_image(image=image, xyxy=offset) for offset in offsets]\n            resolution_wh = get_image_resolution_wh(image)\n\n        batch_callback = cast(\n            Callable[[list[npt.NDArray[Any]]], list[Detections]], self.callback\n        )\n        detections_in_slices = batch_callback(slices)\n        if not isinstance(detections_in_slices, list):\n            raise ValueError(\n                \"Callback must return `list[Detections]` when `batch_size > 1`. \"\n                f\"Got: {type(detections_in_slices)}\"\n            )\n        if len(detections_in_slices) != len(offsets):\n            raise ValueError(\n                f\"Callback returned {len(detections_in_slices)} Detections \"\n                f\"for {len(offsets)} slices. Lengths must match.\"\n            )\n\n        if self.compact_masks:\n            for det, image_slice in zip(detections_in_slices, slices):\n                if det.mask is not None and isinstance(det.mask, np.ndarray):\n                    slice_w, slice_h = get_image_resolution_wh(image_slice)\n                    full_slice_xyxy = np.tile(\n                        np.array([[0, 0, slice_w - 1, slice_h - 1]], dtype=np.float64),\n                        (len(det), 1),\n                    )\n                    det.mask = CompactMask.from_dense(","sourceCodeStart":568,"sourceCodeEnd":604,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/detection/tools/inference_slicer.py#L568-L604","documentation":"Raised by InferenceSlicer's batch path when the callback returns something other than a list while batch_size > 1. In batch mode the callback receives a list of image slices and must return a list with exactly one Detections object per slice; a single Detections (the single-image contract) or any other type cannot be aligned with the slices.","triggerScenarios":"Constructing InferenceSlicer with batch_size=4 but a callback of the form def callback(image) -> sv.Detections (single-image signature); the slicer calls it with a list and the raw non-list result hits this check.","commonSituations":"Upgrading a working single-image pipeline to batching without rewriting the callback; wrapping an ultralytics model.predict call that returns a Results list but converting only the first element.","solutions":["Rewrite the callback for batching: accept a list of images, return [sv.Detections.from_ultralytics(r) for r in model.predict(images, ...)] — one entry per input image, in order.","Alternatively keep the single-image callback and set batch_size=1 (the default).","Ensure the list length equals the input length — see the companion length-mismatch error."],"exampleFix":"# before\ndef callback(image):\n    return sv.Detections.from_ultralytics(model.predict(image, verbose=False)[0])\nslicer = sv.InferenceSlicer(callback=callback, batch_size=8)\n\n# after\ndef callback(images):\n    results = model.predict(images, verbose=False)\n    return [sv.Detections.from_ultralytics(r) for r in results]\nslicer = sv.InferenceSlicer(callback=callback, batch_size=8)","handlingStrategy":"type-guard","validationCode":"def is_batch_callback(cb, n=2) -> bool:\n    import numpy as np\n    probe = [np.zeros((8, 8, 3), dtype=np.uint8) for _ in range(n)]\n    result = cb(probe)\n    return isinstance(result, list) and len(result) == n\n\nif batch_size > 1 and not is_batch_callback(callback):\n    batch_size = 1\nslicer = sv.InferenceSlicer(callback=callback, batch_size=batch_size)","typeGuard":"def returns_detections_list(fn) -> bool:\n    # static check on a probe call with dummy images\n    probe = fn([np.zeros((4, 4, 3), dtype=np.uint8)] * 2)\n    return isinstance(probe, list)","tryCatchPattern":"try:\n    detections = slicer(image)\nexcept ValueError as err:\n    if 'list[Detections]' in str(err):\n        raise RuntimeError('callback must accept and return a list when batch_size > 1') from err\n    raise","preventionTips":["Write the callback as list-in/list-out from the start so batch_size=1 and >1 both work.","Add a smoke test that runs the slicer once with the production batch_size in CI."],"tags":["inference-slicer","batching","callback-contract","valueerror"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}