From cda804cbc0d87a23b8db3742dbaaeb51fa5f02dc Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:37:34 +0530 Subject: [PATCH 1/7] fix: validate command input to prevent execution of untrusted input --- examples/time_in_zone/scripts/stream_from_file.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/time_in_zone/scripts/stream_from_file.py b/examples/time_in_zone/scripts/stream_from_file.py index 4208f4d63..c0937c807 100644 --- a/examples/time_in_zone/scripts/stream_from_file.py +++ b/examples/time_in_zone/scripts/stream_from_file.py @@ -84,7 +84,10 @@ def run_command_in_thread(command: list) -> Thread: def run_command(command: list) -> int: - process = subprocess.run(command) # noqa: S603 # TODO: Validate command input to prevent execution of untrusted input + allowed_commands = ["docker", "ffmpeg"] + if not command or command[0] not in allowed_commands: + raise ValueError(f"Command '{command[0] if command else ''}' is not allowed. Only {allowed_commands} are permitted.") + process = subprocess.run(command) # noqa: S603 return process.returncode From 694f29dd87943224fffab34a148b9396aaa6cf0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:10:13 +0000 Subject: [PATCH 2/7] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/time_in_zone/scripts/stream_from_file.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/time_in_zone/scripts/stream_from_file.py b/examples/time_in_zone/scripts/stream_from_file.py index c0937c807..382fd5f29 100644 --- a/examples/time_in_zone/scripts/stream_from_file.py +++ b/examples/time_in_zone/scripts/stream_from_file.py @@ -86,7 +86,9 @@ def run_command_in_thread(command: list) -> Thread: def run_command(command: list) -> int: allowed_commands = ["docker", "ffmpeg"] if not command or command[0] not in allowed_commands: - raise ValueError(f"Command '{command[0] if command else ''}' is not allowed. Only {allowed_commands} are permitted.") + raise ValueError( + f"Command '{command[0] if command else ''}' is not allowed. Only {allowed_commands} are permitted." + ) process = subprocess.run(command) # noqa: S603 return process.returncode From 0decac30c2ffbddf0cda1a35f48de1071bd152df Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:30:19 +0530 Subject: [PATCH 3/7] Refactor: replace asserts in src/supervision with proper error handling (S101) --- .../time_in_zone/scripts/stream_from_file.py | 3 +- pyproject.toml | 4 +- src/supervision/annotators/core.py | 37 ++++++----- src/supervision/detection/core.py | 64 +++++++++++-------- src/supervision/detection/line_zone.py | 12 ++-- src/supervision/detection/utils/converters.py | 6 +- .../detection/utils/iou_and_nms.py | 45 +++++++------ src/supervision/detection/vlm.py | 17 +++-- src/supervision/key_points/annotators.py | 18 ++++-- .../metrics/mean_average_precision.py | 5 +- .../byte_tracker/single_object_track.py | 19 +++--- src/supervision/utils/image.py | 18 ++++-- tests/detection/utils/test_converters.py | 8 +-- 13 files changed, 147 insertions(+), 109 deletions(-) diff --git a/examples/time_in_zone/scripts/stream_from_file.py b/examples/time_in_zone/scripts/stream_from_file.py index 382fd5f29..c941c92c6 100644 --- a/examples/time_in_zone/scripts/stream_from_file.py +++ b/examples/time_in_zone/scripts/stream_from_file.py @@ -87,7 +87,8 @@ def run_command(command: list) -> int: allowed_commands = ["docker", "ffmpeg"] if not command or command[0] not in allowed_commands: raise ValueError( - f"Command '{command[0] if command else ''}' is not allowed. Only {allowed_commands} are permitted." + f"Command '{command[0] if command else ''}' is not allowed. " + f"Only {allowed_commands} are permitted." ) process = subprocess.run(command) # noqa: S603 return process.returncode diff --git a/pyproject.toml b/pyproject.toml index b942c21a0..94c4236a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,9 +158,7 @@ lint.per-file-ignores."notebooks/**" = [ "PT018", # Assertion should be broken down into multiple parts "S101", # Use of `assert` detected ] -lint.per-file-ignores."src/**" = [ - "S101", # TODO: Replace asserts with proper error handling -] + lint.per-file-ignores."tests/**" = [ "S101", # Use of `assert` detected ] diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index 559528cc6..182658dd8 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -1360,11 +1360,12 @@ def _draw_labels( detections: Detections, custom_color_lookup: npt.NDArray[np.int_] | None, ) -> None: - assert len(labels) == len(label_properties) == len(detections), ( - f"Number of label properties ({len(label_properties)}), " - f"labels ({len(labels)}) and detections ({len(detections)}) " - "do not match." - ) + if not (len(labels) == len(label_properties) == len(detections)): + raise ValueError( + f"Number of label properties ({len(label_properties)}), " + f"labels ({len(labels)}) and detections ({len(detections)}) " + "do not match." + ) color_lookup = ( custom_color_lookup @@ -1584,7 +1585,8 @@ def annotate( ``` """ - assert isinstance(scene, Image.Image) + if not isinstance(scene, Image.Image): + raise TypeError("scene must be a PIL Image") _validate_labels(labels, detections) draw = ImageDraw.Draw(scene) @@ -1674,11 +1676,12 @@ def _draw_labels( detections: Detections, custom_color_lookup: npt.NDArray[np.int_] | None, ) -> None: - assert len(labels) == len(label_properties) == len(detections), ( - f"Number of label properties ({len(label_properties)}), " - f"labels ({len(labels)}) and detections ({len(detections)}) " - "do not match." - ) + if not (len(labels) == len(label_properties) == len(detections)): + raise ValueError( + f"Number of label properties ({len(label_properties)}), " + f"labels ({len(labels)}) and detections ({len(detections)}) " + "do not match." + ) color_lookup = ( custom_color_lookup if custom_color_lookup is not None @@ -2656,7 +2659,8 @@ def annotate( if custom_values is not None: value = custom_values[detection_idx] else: - assert detections.confidence is not None # MyPy type hint + if detections.confidence is None: + raise ValueError("detections.confidence cannot be None") value = detections.confidence[detection_idx] color = resolve_color( @@ -3127,7 +3131,8 @@ def annotate( @staticmethod def _use_obb(detections_1: Detections, detections_2: Detections) -> bool: - assert not detections_1.is_empty() or not detections_2.is_empty() + if detections_1.is_empty() and detections_2.is_empty(): + raise ValueError("At least one of the detections must not be empty") is_obb_1 = ORIENTED_BOX_COORDINATES in detections_1.data is_obb_2 = ORIENTED_BOX_COORDINATES in detections_2.data return ( @@ -3138,7 +3143,8 @@ def _use_obb(detections_1: Detections, detections_2: Detections) -> bool: @staticmethod def _use_mask(detections_1: Detections, detections_2: Detections) -> bool: - assert not detections_1.is_empty() or not detections_2.is_empty() + if detections_1.is_empty() and detections_2.is_empty(): + raise ValueError("At least one of the detections must not be empty") is_mask_1 = detections_1.mask is not None is_mask_2 = detections_2.mask is not None return ( @@ -3185,7 +3191,8 @@ def _mask_from_mask( mask = np.zeros(scene.shape[:2], dtype=np.bool_) if detections.is_empty(): return mask - assert detections.mask is not None + if detections.mask is None: + raise ValueError("detections.mask cannot be None") for detections_mask in detections.mask: mask |= detections_mask.astype(np.bool_) diff --git a/src/supervision/detection/core.py b/src/supervision/detection/core.py index 4f38f1e59..4d9e26009 100644 --- a/src/supervision/detection/core.py +++ b/src/supervision/detection/core.py @@ -1883,7 +1883,8 @@ def from_vlm( vlm = _validate_vlm_parameters(vlm, result, kwargs) if vlm == VLM.PALIGEMMA: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") xyxy, class_id, class_name = from_paligemma(result, **kwargs) data: dict[str, npt.NDArray[np.generic] | list[Any]] = { CLASS_NAME_DATA_FIELD: class_name, @@ -1891,7 +1892,8 @@ def from_vlm( return cls(xyxy=xyxy, class_id=class_id, data=data) if vlm == VLM.QWEN_2_5_VL: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs) data = {CLASS_NAME_DATA_FIELD: class_name} confidence_arr: npt.NDArray[np.floating[Any]] = np.ones( @@ -1902,7 +1904,8 @@ def from_vlm( ) if vlm == VLM.QWEN_3_VL: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") xyxy, class_id, class_name = from_qwen_3_vl(result, **kwargs) data = {CLASS_NAME_DATA_FIELD: class_name} confidence_arr = np.ones(len(xyxy), dtype=float) @@ -1911,13 +1914,15 @@ def from_vlm( ) if vlm == VLM.DEEPSEEK_VL_2: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") xyxy, class_id, class_name = from_deepseek_vl_2(result, **kwargs) data = {CLASS_NAME_DATA_FIELD: class_name} return cls(xyxy=xyxy, class_id=class_id, data=data) if vlm == VLM.FLORENCE_2: - assert isinstance(result, dict) + if not isinstance(result, dict): + raise TypeError("result must be a dict") xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs) if len(xyxy) == 0: empty = cls.empty() @@ -1933,18 +1938,21 @@ def from_vlm( return cls(xyxy=xyxy, mask=mask, data=data) if vlm == VLM.GOOGLE_GEMINI_2_0: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") xyxy, class_id, class_name = from_google_gemini_2_0(result, **kwargs) data = {CLASS_NAME_DATA_FIELD: class_name} return cls(xyxy=xyxy, class_id=class_id, data=data) if vlm == VLM.MOONDREAM: - assert isinstance(result, dict) + if not isinstance(result, dict): + raise TypeError("result must be a dict") xyxy = from_moondream(result, **kwargs) return cls(xyxy=xyxy) if vlm == VLM.GOOGLE_GEMINI_2_5: - assert isinstance(result, str) + if not isinstance(result, str): + raise TypeError("result must be a string") gemini_result = from_google_gemini_2_5(result, **kwargs) data = {CLASS_NAME_DATA_FIELD: gemini_result[2]} return cls( @@ -2487,23 +2495,25 @@ def with_nms( after non-maximum suppression. Raises: - AssertionError: If `confidence` is None and class_agnostic is False. + ValueError: If `confidence` is None and class_agnostic is False. If `class_id` is None and class_agnostic is False. """ if len(self) == 0: return self - assert self.confidence is not None, ( - "Detections confidence must be given for NMS to be executed." - ) + if self.confidence is None: + raise ValueError( + "Detections confidence must be given for NMS to be executed." + ) if class_agnostic: predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) else: - assert self.class_id is not None, ( - "Detections class_id must be given for NMS to be executed. If you" - " intended to perform class agnostic NMS set class_agnostic=True." - ) + if self.class_id is None: + raise ValueError( + "Detections class_id must be given for NMS to be executed. If you" + " intended to perform class agnostic NMS set class_agnostic=True." + ) predictions = np.hstack( ( self.xyxy, @@ -2574,7 +2584,7 @@ def with_nmm( Groups of size 1 keep the original OBB unchanged. Raises: - AssertionError: If `confidence` is None or `class_id` is None and + ValueError: If `confidence` is None or `class_id` is None and class_agnostic is False. ![non-max-merging](https://media.roboflow.com/supervision-docs/non-max-merging.png){ align=center width="800" } @@ -2582,17 +2592,19 @@ def with_nmm( if len(self) == 0: return self - assert self.confidence is not None, ( - "Detections confidence must be given for NMM to be executed." - ) + if self.confidence is None: + raise ValueError( + "Detections confidence must be given for NMM to be executed." + ) if class_agnostic: predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1))) else: - assert self.class_id is not None, ( - "Detections class_id must be given for NMM to be executed. If you" - " intended to perform class agnostic NMM set class_agnostic=True." - ) + if self.class_id is None: + raise ValueError( + "Detections class_id must be given for NMM to be executed. If you" + " intended to perform class agnostic NMM set class_agnostic=True." + ) predictions = np.hstack( ( self.xyxy, @@ -2815,8 +2827,8 @@ def merge_inner_detection_object_pair( if detections_1.confidence is None and detections_2.confidence is None: merged_confidence = None else: - assert detections_1.confidence is not None - assert detections_2.confidence is not None + if detections_1.confidence is None or detections_2.confidence is None: + raise ValueError("confidence cannot be None") detection_1_area = (xyxy_1[2] - xyxy_1[0]) * (xyxy_1[3] - xyxy_1[1]) detections_2_area = (xyxy_2[2] - xyxy_2[0]) * (xyxy_2[3] - xyxy_2[1]) merged_confidence = ( diff --git a/src/supervision/detection/line_zone.py b/src/supervision/detection/line_zone.py index 3178fca24..40db86783 100644 --- a/src/supervision/detection/line_zone.py +++ b/src/supervision/detection/line_zone.py @@ -281,8 +281,10 @@ def _compute_anchor_sides( The third array, `has_any_right_trigger`, indicates if the detection's anchor is on the right side of the line zone. """ - assert len(detections) > 0 - assert detections.tracker_id is not None + if len(detections) == 0: + raise ValueError("Detections must not be empty.") + if detections.tracker_id is None: + raise ValueError("Detections must have tracker_id.") all_anchors = np.array( [ @@ -312,7 +314,8 @@ def _update_class_id_to_name(self, detections: Detections) -> None: Assumes that class_names are only provided when class_ids are. """ class_names = detections.data.get(CLASS_NAME_DATA_FIELD) - assert class_names is None or detections.class_id is not None + if class_names is not None and detections.class_id is None: + raise ValueError("class_names provided but detections.class_id is None.") if detections.class_id is None: return @@ -618,7 +621,8 @@ def _draw_oriented_label( text_box_color=self.color, line_angle_degrees=line_angle_degrees, ) - assert label_image.shape[0] == label_image.shape[1] + if label_image.shape[0] != label_image.shape[1]: + raise ValueError("Label image must be square.") text_width, text_height = cv2.getTextSize( text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness diff --git a/src/supervision/detection/utils/converters.py b/src/supervision/detection/utils/converters.py index 9626daf6d..dcaf1ca55 100644 --- a/src/supervision/detection/utils/converters.py +++ b/src/supervision/detection/utils/converters.py @@ -691,8 +691,10 @@ def mask_to_rle( ![mask_to_rle](https://media.roboflow.com/supervision-docs/ mask-to-rle.png){ align=center width="800" } """ - assert mask.ndim == 2, "Input mask must be 2D" - assert mask.size != 0, "Input mask cannot be empty" + if mask.ndim != 2: + raise ValueError("Input mask must be 2D") + if mask.size == 0: + raise ValueError("Input mask cannot be empty") counts: list[int] = cast(list[int], _mask_to_rle_counts(mask).tolist()) if compressed: diff --git a/src/supervision/detection/utils/iou_and_nms.py b/src/supervision/detection/utils/iou_and_nms.py index 9b367c141..e2c087489 100644 --- a/src/supervision/detection/utils/iou_and_nms.py +++ b/src/supervision/detection/utils/iou_and_nms.py @@ -345,9 +345,8 @@ def box_iou_batch_with_jaccard( ``` """ - assert len(is_crowd) == len(boxes_true), ( - "`is_crowd` must have the same length as `boxes_true`" - ) + if len(is_crowd) != len(boxes_true): + raise ValueError("`is_crowd` must have the same length as `boxes_true`") if len(boxes_detection) == 0 or len(boxes_true) == 0: return cast(npt.NDArray[np.float64], np.array([])) ious: npt.NDArray[np.float64] = np.zeros( @@ -828,13 +827,14 @@ def mask_non_max_suppression( non-maximum suppression. Raises: - AssertionError: If `iou_threshold` is not within the closed + ValueError: If `iou_threshold` is not within the closed range from `0` to `1`. """ - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) + if not (0 <= iou_threshold <= 1): + raise ValueError( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) rows, columns = predictions.shape if columns == 5: @@ -921,13 +921,14 @@ def box_non_max_suppression( non-maximum suppression. Raises: - AssertionError: If `iou_threshold` is not within the + ValueError: If `iou_threshold` is not within the closed range from `0` to `1`. """ - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) + if not (0 <= iou_threshold <= 1): + raise ValueError( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) sort_index, predictions, categories = _prepare_predictions_for_nms(predictions) ious = box_iou_batch(predictions[:, :4], predictions[:, :4], overlap_metric) keep = _nms_loop_from_iou_matrix(ious, categories, iou_threshold) @@ -1256,10 +1257,11 @@ def oriented_box_non_max_suppression( >>> keep array([ True, False]) """ - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) + if not (0 <= iou_threshold <= 1): + raise ValueError( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) for name, arr in (("predictions", predictions), ("oriented_boxes", oriented_boxes)): if name == "predictions": if arr.ndim != 2 or arr.shape[1] not in (5, 6): @@ -1401,10 +1403,11 @@ class are used by the grouping logic; overlap is computed on f"`predictions` and `oriented_boxes` must have the same length, " f"got {len(predictions)} and {len(oriented_boxes)}." ) - assert 0 <= iou_threshold <= 1, ( - "Value of `iou_threshold` must be in the closed range from 0 to 1, " - f"{iou_threshold} given." - ) + if not (0 <= iou_threshold <= 1): + raise ValueError( + "Value of `iou_threshold` must be in the closed range from 0 to 1, " + f"{iou_threshold} given." + ) def group_within(global_indices: npt.NDArray[np.int_]) -> list[list[int]]: return _group_overlapping_oriented_boxes( diff --git a/src/supervision/detection/vlm.py b/src/supervision/detection/vlm.py index 0b0bc830a..c83afeab8 100644 --- a/src/supervision/detection/vlm.py +++ b/src/supervision/detection/vlm.py @@ -515,7 +515,8 @@ def from_florence_2( `obb_boxes` is an optional array of shape `(n, 4, 2)` with oriented bounding boxes. """ - assert len(result) == 1, f"Expected result with a single element. Got: {result}" + if len(result) != 1: + raise ValueError(f"Expected result with a single element. Got: {result}") task = next(iter(result.keys())) if task not in SUPPORTED_TASKS_FLORENCE_2: raise ValueError( @@ -564,18 +565,20 @@ def from_florence_2( return xyxy, labels, None, None if task in ["", ""]: - assert isinstance(result, str), ( - f"Expected string as result, got {type(result)}" - ) + if not isinstance(result, str): + raise TypeError( + f"Expected string as result, got {type(result)}" + ) if result == "No object detected.": return np.empty((0, 4), dtype=np.float32), np.array([]), None, None pattern = re.compile(r"") match = pattern.search(result) - assert match is not None, ( - f"Expected string to end in location tags, but got {result}" - ) + if match is None: + raise ValueError( + f"Expected string to end in location tags, but got {result}" + ) w, h = _validate_resolution(resolution_wh) xyxy = np.array([match.groups()], dtype=np.float32) diff --git a/src/supervision/key_points/annotators.py b/src/supervision/key_points/annotators.py index 63a2b7d1a..64d8864f2 100644 --- a/src/supervision/key_points/annotators.py +++ b/src/supervision/key_points/annotators.py @@ -84,7 +84,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") if len(key_points) == 0: return scene @@ -205,7 +206,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") if len(key_points) == 0: return scene @@ -445,7 +447,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") if len(key_points) == 0: return scene @@ -544,7 +547,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") if len(key_points) == 0: return scene @@ -645,7 +649,8 @@ def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType: ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") if len(key_points) == 0: return scene @@ -824,7 +829,8 @@ def annotate( ``` """ - assert isinstance(scene, np.ndarray) + if not isinstance(scene, np.ndarray): + raise TypeError("scene must be an np.ndarray") font = cv2.FONT_HERSHEY_SIMPLEX skeletons_count, points_count, _ = key_points.xy.shape diff --git a/src/supervision/metrics/mean_average_precision.py b/src/supervision/metrics/mean_average_precision.py index 8d598da9c..be09942c2 100644 --- a/src/supervision/metrics/mean_average_precision.py +++ b/src/supervision/metrics/mean_average_precision.py @@ -481,9 +481,8 @@ def load_predictions(self, predictions: list[dict[str, Any]]) -> EvaluationDatas ids = [pred["image_id"] for pred in predictions] # Make sure the image ids from predictions exist in the current dataset - assert set(ids) == (set(ids) & set(self.get_image_ids())), ( - "Results do not correspond to current coco set" - ) + if set(ids) != (set(ids) & set(self.get_image_ids())): + raise ValueError("Results do not correspond to current coco set") # Check if the predictions contain any unsupported keys if "caption" in predictions[0]: diff --git a/src/supervision/tracker/byte_tracker/single_object_track.py b/src/supervision/tracker/byte_tracker/single_object_track.py index 26f6bb683..2b1fbbcd1 100644 --- a/src/supervision/tracker/byte_tracker/single_object_track.py +++ b/src/supervision/tracker/byte_tracker/single_object_track.py @@ -49,9 +49,8 @@ def __init__( self.external_track_id = self.external_id_counter.NO_ID def predict(self) -> None: - assert self.mean is not None - assert self.covariance is not None - assert self.kalman_filter is not None + if self.mean is None or self.covariance is None or self.kalman_filter is None: + raise ValueError("mean, covariance, and kalman_filter must not be None") mean_state = self.mean.copy() if self.state != TrackState.Tracked: mean_state[7] = 0 @@ -65,8 +64,8 @@ def multi_predict(stracks: list[STrack], shared_kalman: KalmanFilter) -> None: multi_mean = [] multi_covariance = [] for i, st in enumerate(stracks): - assert st.mean is not None - assert st.covariance is not None + if st.mean is None or st.covariance is None: + raise ValueError("st.mean and st.covariance must not be None") multi_mean.append(st.mean.copy()) multi_covariance.append(st.covariance) if st.state != TrackState.Tracked: @@ -98,9 +97,8 @@ def activate(self, kalman_filter: KalmanFilter, frame_id: int) -> None: self.start_frame = frame_id def re_activate(self, new_track: STrack, frame_id: int) -> None: - assert self.kalman_filter is not None - assert self.mean is not None - assert self.covariance is not None + if self.kalman_filter is None or self.mean is None or self.covariance is None: + raise ValueError("kalman_filter, mean, and covariance must not be None") self.mean, self.covariance = self.kalman_filter.update( self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh) ) @@ -118,9 +116,8 @@ def update(self, new_track: STrack, frame_id: int) -> None: new_track: The new track data. frame_id: The current frame ID. """ - assert self.kalman_filter is not None - assert self.mean is not None - assert self.covariance is not None + if self.kalman_filter is None or self.mean is None or self.covariance is None: + raise ValueError("kalman_filter, mean, and covariance must not be None") self.frame_id = frame_id self.tracklet_len += 1 diff --git a/src/supervision/utils/image.py b/src/supervision/utils/image.py index a4768e948..5a675efea 100644 --- a/src/supervision/utils/image.py +++ b/src/supervision/utils/image.py @@ -132,7 +132,8 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType: ![scale-image](https://media.roboflow.com/supervision-docs/supervision-docs-scale-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - assert isinstance(image, np.ndarray) + if not isinstance(image, np.ndarray): + raise TypeError("image must be a np.ndarray") if scale_factor <= 0: raise ValueError("Scale factor must be positive.") @@ -190,7 +191,8 @@ def resize_image( ![resize-image](https://media.roboflow.com/supervision-docs/supervision-docs-resize-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - assert isinstance(image, np.ndarray) + if not isinstance(image, np.ndarray): + raise TypeError("image must be a np.ndarray") if keep_aspect_ratio: image_ratio = image.shape[1] / image.shape[0] target_ratio = resolution_wh[0] / resolution_wh[1] @@ -252,7 +254,8 @@ def letterbox_image( ![letterbox-image](https://media.roboflow.com/supervision-docs/supervision-docs-letterbox-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - assert isinstance(image, np.ndarray) + if not isinstance(image, np.ndarray): + raise TypeError("image must be a np.ndarray") color = unify_to_bgr(color=color) resized_image = resize_image( image=image, resolution_wh=resolution_wh, keep_aspect_ratio=True @@ -389,7 +392,8 @@ def tint_image( ![tint-image](https://media.roboflow.com/supervision-docs/supervision-docs-tint-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - assert isinstance(image, np.ndarray) + if not isinstance(image, np.ndarray): + raise TypeError("image must be a np.ndarray") if not 0.0 <= opacity <= 1.0: raise ValueError("opacity must be between 0.0 and 1.0") @@ -744,11 +748,13 @@ def _establish_grid_size( return _negotiate_grid_size(images=images) if grid_size[0] is None: columns = grid_size[1] - assert columns is not None + if columns is None: + raise ValueError("columns must be provided if grid_size is None") return math.ceil(len(images) / columns), columns if grid_size[1] is None: rows = grid_size[0] - assert rows is not None + if rows is None: + raise ValueError("rows must be provided if grid_size is None") return rows, math.ceil(len(images) / rows) return cast(tuple[int, int], grid_size) diff --git a/tests/detection/utils/test_converters.py b/tests/detection/utils/test_converters.py index 167ac2e26..245f286d7 100644 --- a/tests/detection/utils/test_converters.py +++ b/tests/detection/utils/test_converters.py @@ -375,14 +375,14 @@ def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) -> np.array([[[]]]).astype(bool), False, None, - pytest.raises(AssertionError, match="Input mask must be 2D"), - ), # raises AssertionError because mask dimensionality is not 2D + pytest.raises(ValueError, match="Input mask must be 2D"), + ), # raises ValueError because mask dimensionality is not 2D ( np.array([[]]).astype(bool), False, None, - pytest.raises(AssertionError, match="Input mask cannot be empty"), - ), # raises AssertionError because mask is empty + pytest.raises(ValueError, match="Input mask cannot be empty"), + ), # raises ValueError because mask is empty ], ) def test_mask_to_rle( From 1b6a3ce0dcaf6792e1c47ff06bf8df1485467671 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:03:19 +0000 Subject: [PATCH 4/7] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 94c4236a3..b380f5ddf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,7 +158,6 @@ lint.per-file-ignores."notebooks/**" = [ "PT018", # Assertion should be broken down into multiple parts "S101", # Use of `assert` detected ] - lint.per-file-ignores."tests/**" = [ "S101", # Use of `assert` detected ] From 269a74bd9209df58202a3ede8b605113baaa1afa Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:56:02 +0530 Subject: [PATCH 5/7] fix(lint): shorten ValueError message to comply with E501 line length limit --- examples/time_in_zone/scripts/stream_from_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/time_in_zone/scripts/stream_from_file.py b/examples/time_in_zone/scripts/stream_from_file.py index c941c92c6..4fd2bf316 100644 --- a/examples/time_in_zone/scripts/stream_from_file.py +++ b/examples/time_in_zone/scripts/stream_from_file.py @@ -87,8 +87,8 @@ def run_command(command: list) -> int: allowed_commands = ["docker", "ffmpeg"] if not command or command[0] not in allowed_commands: raise ValueError( - f"Command '{command[0] if command else ''}' is not allowed. " - f"Only {allowed_commands} are permitted." + f"Invalid command '{command[0] if command else ''}'. " + f"Allowed: {allowed_commands}" ) process = subprocess.run(command) # noqa: S603 return process.returncode From c86eeb396a698bccdc2cef6411c56c46ce483152 Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:42:57 +0530 Subject: [PATCH 6/7] Fix: replace remaining assert with ValueError in annotators/core.py --- src/supervision/annotators/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index 22fc5f78c..c07be344e 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -781,7 +781,8 @@ def annotate( self.color_lookup if custom_color_lookup is None else custom_color_lookup, collect_union=True, ) - assert fmask is not None # collect_union=True always returns an array + if fmask is None: + raise ValueError("fmask cannot be None when collect_union=True") colored_mask = cv2.blur(colored_mask, (self.kernel_size, self.kernel_size)) colored_mask[fmask] = [0, 0, 0] From 1272c4b1c2f134e8873e5426af7a460c0ef4be79 Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:29:18 +0530 Subject: [PATCH 7/7] fix: add opacity validation to Mask, Color, and Halo annotators --- src/supervision/annotators/core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index c07be344e..24a4e9742 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -89,7 +89,7 @@ class _BaseLabelAnnotator(BaseAnnotator): color_lookup: The method used to determine the color of the label. text_color: The color to use for the label text. text_padding: The padding around the label text, in pixels. - text_anchor: The position of the text relative to the detection + text_position: The position of the text relative to the detection bounding box. text_offset: A tuple of 2D coordinates `(x, y)` to offset the text position from the anchor point, in pixels. @@ -444,6 +444,8 @@ def __init__( Options are `INDEX`, `CLASS`, `TRACK`. """ self.color: Color | ColorPalette = _normalize_color_input(color) + if not 0.0 <= opacity <= 1.0: + raise ValueError("Opacity must be between 0.0 and 1.0") self.opacity = opacity self.color_lookup: ColorLookup = color_lookup @@ -624,6 +626,8 @@ def __init__( """ self.color: Color | ColorPalette = _normalize_color_input(color) self.color_lookup: ColorLookup = color_lookup + if not 0.0 <= opacity <= 1.0: + raise ValueError("Opacity must be between 0.0 and 1.0") self.opacity = opacity @ensure_cv2_image_for_class_method @@ -722,6 +726,8 @@ def __init__( Options are `INDEX`, `CLASS`, `TRACK`. """ self.color: Color | ColorPalette = _normalize_color_input(color) + if not 0.0 <= opacity <= 1.0: + raise ValueError("Opacity must be between 0.0 and 1.0") self.opacity = opacity self.color_lookup: ColorLookup = color_lookup self.kernel_size: int = kernel_size