Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save lagergren/043b138c320276f74e9d6d2afba2bfee to your computer and use it in GitHub Desktop.

Select an option

Save lagergren/043b138c320276f74e9d6d2afba2bfee to your computer and use it in GitHub Desktop.
Pyright strict-mode real-bug A/B/C classification for sics-ai/zombiesnack PR #2291

Pyright strict-mode real-bug audit — A/B/C classification

Follow-up to the original strict-mode real-bug audit attached to PR #2291. Strict mode is not enabled in the PR — this is a one-off audit run by temporarily flipping typeCheckingMode = "strict" and reverting before commit.

The original audit listed 187 primary error sites for the two remaining real-bug rules after reportIndexIssue was eliminated in commit 4f0e2237b:

  • 161 × reportUnnecessaryComparison
  • 26 × reportConstantRedefinition

This report classifies each site into one of three buckets:

  • (A) Real bug — pyright found genuine dead code, an always-true/always-false branch, or a constant redefined with a different value. Fix is code-level: delete the dead branch, fix the wrong-direction comparison, correct the redefinition.
  • (B) Type annotation too narrow — pyright thinks the value is narrower than runtime allows. The branch IS reachable at runtime, but the static types don't capture that. Fix is annotation-level: widen a parameter, mark a "constant" as a regular module-level variable, use cast at a boundary.
  • (C) Defensive belt-and-suspenders — pyright proves the check is statically redundant, but it guards against runtime values pyright doesn't model: data from JSON / SQL / network, Any from external libs, mocks in tests, dynamic config. Keep the check; suppress with # pyright: ignore[<rule>] and a one-line comment explaining what runtime path could violate the static assumption.

Headline numbers

Bucket Count Share
A real bug 53 28%
B annotation too narrow 111 59%
C defensive 23 12%
Total 187

The dominant pattern is (B): too-narrow annotations — fields/params declared non-Optional that are runtime-Optional, single-assign init flags written ALL_CAPS that pyright sees as Final, and external library stubs that don't model their real Optional-or-raise contract (cv2, paramiko, GitPython, google-auth, pyads, OPCUA wire types). Fixing these is annotation-level and shouldn't need any runtime behaviour change.

The (A) cohort is concentrated in:

  • src/pap_rob (15 — mostly redundant is not None after asserts/early returns, plus dead checks against @property returns and module-level constants),
  • src/camera (12 — frameset-returning helpers typed non-Optional that genuinely never return None, plus aiohttp.read() / bytes confusion),
  • src/hyperstar (12 — three ip_scale ternaries, four get_robot_name ternaries, three already-narrowed-by-assert sites).

The (C) cohort is small and concentrated in two places: pap_rob's cast(dict, request.json) Flask handler pattern (8 sites — really one bug-class with a typed-helper fix) and the bq_client_query wrapper used across infra (3 sites).

Per-module summary

Module Total A (bug) B (annotation) C (defensive)
src/hyperstar 46 12 29 5
src/pap_rob 39 15 14 10
src/camera 23 12 11 0
src/robots 13 0 11 2
tests/integration 13 0 11 2
src/data_engine 11 4 7 0
src/image 9 0 9 0
src/infra 8 4 1 3
src/synq_api 5 0 5 0
tests/unit 5 0 4 1
src/utils 4 2 2 0
src/simulation 3 2 1 0
src/camera_tools 2 0 2 0
src/automation_tasks 1 0 1 0
src/barcodescanner 1 1 0 0
src/deploy 1 0 1 0
src/devops 1 0 1 0
src/frameset_tools 1 1 0 0
src/viz_server 1 0 1 0
Total 187 53 111 23

High-leverage fixes

A handful of single-fix changes resolve a large fraction of the report:

  1. JointTimeline.qpos / JointTimeline.action| None — resolves 6 sites (src/hyperstar/intent_editor/auto_intent.py ×4, auto_trim_service.py ×2). Same dataclass widening.
  2. Single-assign init flags re-annotated without Final — resolves 8 sites across hyperstar (_PATCHED, _DISCOVERED, GFX_BACKEND_INITIALIZED, _HYPERSTAR_TYPES_IMPORTED, HAS_DATEUTIL, _REQUEST_TOPIC, _RESULT_TOPIC, _X). Same naming-rule pattern.
  3. pap_rob_api.py JSON-body cast helper — resolves 8 sites in wms_interaction_api.py + pap_rob_api.py if cast(dict, request.json) is replaced with a typed helper that returns dict | None.
  4. world.robot_controller: RobotController | None — resolves 3 sites in pap_rob/lessons/* (lesson teardown can run before init completes).
  5. DETR-style C, H, W = ... axis-name unpacks renamed lowercase — resolves 8 sites across src/data_engine/models/matcher.py, src/camera/parameters.py, src/camera/femtomega/*, src/camera_tools/*, tests/unit/image/transformers/hue_transformer_test.py. Same naming-convention violation.
  6. pyads_bugfixes.py PLC datatype dispatch annotation widened — resolves 6 sites in src/robots/impl/kuka/ads/network/pyads_bugfixes.py (type(plc_datatype) is tuple pattern).
  7. Frame.data / Frame.depth_data / Frame.color_data raise-vs-Optional duality — resolves ~5 sites in src/camera/calibration.py and src/camera/frameset.py if the public properties are clarified (or callers move to the .depth / .color Optional accessors).

These 7 fix-clusters cover 44 of the 187 sites (24%) with maybe a dozen actual code changes between them.

Per-error detail (grouped by module)

src/automation_tasks

  • B src/automation_tasks/envelope_pick_and_place/main.py:60SicsformerRvModel.request_result is annotated -> PickAndPlaceModelResult but the caller defensively guards against null results alongside not res.actions. Widen the base method return to PickAndPlaceModelResult | None.

src/barcodescanner

  • A src/barcodescanner/barcodescanner.py:126dev_file.read(struct_len) on a binary file always returns bytes, possibly empty b'' at EOF. The if data is not None guard is dead; if the intent is to skip short reads, change to if len(data) == struct_len: (or if data:).

src/camera

  • B src/camera/calibration.py:178math_calibrate_extrinsics is annotated -> CameraExtrinsics but actually returns False on failure; widen its return to CameraExtrinsics | Literal[False] (or | None).
  • A src/camera/calibration.py:350filter_valid_aruco returns tuple[NDArray, NDArray] (never None); drop the aruco_pixel_d_coordinates is None check.
  • A src/camera/calibration.py:491CameraExtrinsics.framesets: list[FrameSet] with default [], never None; drop the framesets is None half of the guard.
  • B src/camera/calibration.py:495frameset.depth_data is the raise-on-missing property typed NDArray; either use .depth (Optional) or treat the comparison as a defensive widening.
  • B src/camera/calibration.py:527 — same as :495 but for frameset.color_data; should access .color (Optional) or widen.
  • A src/camera/camera.py:54camera_id = camera_config.name and name: str is never None on the dataclass; drop the dead guard.
  • A src/camera/cameras_list.py:222config.calibrated: bool = True can never be None; logic was probably meant to be a different attribute or is False.
  • B src/camera/femtomega/femtotest.py:93 (WIDTH) — script-local pseudo-constant; rename to lowercase or annotate non-Final.
  • B src/camera/femtomega/femtotest.py:93 (HEIGHT) — same pattern.
  • A src/camera/femtomega/femtotest.py:105imgsrc.next_frameset() is typed -> FrameSet; implementations raise rather than return None. Drop the dead None check.
  • A src/camera/femtomega/imagesource.py:98self._femtomega was just verified non-None at line 92 with no reassignment; the second guard is dead.
  • A src/camera/femtomega/imagesource.py:170FemtoMega.intrinsics() returns dict[str, Any] ({} on failure, never None); replace with if not device_intrinsics:.
  • A src/camera/femtomega/imagesource_example.py:36next_frameset() typed -> FrameSet and never returns None at runtime; drop dead branch.
  • B src/camera/femtomega/imagesource_example.py:42 (HEIGHT) — script-local pseudo-constant.
  • B src/camera/femtomega/imagesource_example.py:42 (WIDTH) — same.
  • B src/camera/femtomega/imagesource_example.py:43 (HEIGHT) — same.
  • B src/camera/femtomega/imagesource_example.py:43 (WIDTH) — same.
  • A src/camera/fetch_frameset.py:29Camera.get_latest_frameset() is typed -> FrameSet (never None); the None guard is dead. (Widen if you actually want this defense.)
  • B src/camera/frameset.py:419_single_frame_only(...).data is the raise-on-missing property typed NDArray; widen or use _data directly to keep the defensive None check.
  • A src/camera/imagefile/filebased_imagesource.py:128self.loaded_files = loaded_files or [] always yields a list, so is not None is always True; check should be if self.loaded_files: (truthiness).
  • B src/camera/parameters.py:474 — field rvec: NDArray but __init__ does self.rvec = kwargs.get('rvec') which can be None; widen field to NDArray | None.
  • B src/camera/parameters.py:477M: NDArray | None = None flagged as constant due to upper-case name; rename or # pyright: ignore[reportConstantRedefinition].
  • A src/camera/rest_imagesource.py:22aiohttp.ClientResponse.read() returns bytes (never None); drop the dead data is None check.

src/camera_tools

  • B src/camera_tools/generate_video_from_imagesource_snapshots.py:533 (H) — script-local pseudo-constant from color_frame.shape unpack; rename to lowercase or # pyright: ignore[reportConstantRedefinition] (the existing # noqa: N806 already concedes this is not a real constant).
  • B src/camera_tools/generate_video_from_imagesource_snapshots.py:533 (W) — same.

src/data_engine

  • B src/data_engine/dashboard/image_api.py:61frameset.get_data('color', ...) is annotated NDArray but at runtime returns None when cv2 decode fails or camera_id missing. Widen FrameSet.get_data return to NDArray | None.
  • B src/data_engine/dataset/resolve_rois.py:126pre_color_frame.data is ndarray per annotation, but cv2 decode can leave data as None on decode failure. Widen Frame.data to NDArray | None.
  • B src/data_engine/dataset/resolve_rois.py:126 (col 38) — same site, second clause (post_color_data is None); same fix.
  • B src/data_engine/health_db.py:161_known_faulty_frameset_ids is module-level set[UUID] but used as a "lazy-init" sentinel via is None. The annotation needs set[UUID] | None (or refactor to a separate _loaded flag).
  • B src/data_engine/models/matcher.py:93C is the classic DETR cost-matrix math name, not a constant. Rename to lowercase c (or cost) to silence the rule.
  • B src/data_engine/models/matcher.py:158 — same case in second matcher class; same fix.
  • A src/data_engine/processing/batch_processor.py:265frameset_row is asserted truthy at line 253 and the unpacking returns PrePostActionFramesetsRow (non-Optional). Drop the redundant and frameset_row is not None.
  • A src/data_engine/processing/batch_processor.py:266labeling_row typed non-Optional from _create_database_rows; trailing and labeling_row is not None is always True. Drop.
  • A src/data_engine/processing/batch_processor.py:269 — same dead guard on frameset_row. Simplify.
  • A src/data_engine/processing/batch_processor.py:271 — same dead guard on labeling_row. Simplify.
  • B src/data_engine/roi_data_miner/extract_env_data.py:937downloader.get_frameset(frameset_id) annotated -> FrameSet but realistically returns None if id wasn't downloaded (surrounding code logs "not found after download"). Widen FrameSetDownloader.get_frameset return to FrameSet | None.

src/deploy

  • B src/deploy/launchers/lumi_ddp.py:816content = f.read() is on a paramiko SFTPFile; paramiko's stubs say bytes, but in practice SFTP reads can yield None on transient channel issues. Widen the local annotation or # pyright: ignore[reportUnnecessaryComparison] (paramiko-typing gap).

src/devops

  • B src/devops/terraform/common.py:34GitRepo.working_dir is typed str | PathLike[str] by GitPython but is genuinely Optional[PathLike] at runtime (e.g. bare repos), which is exactly why this code raises InvalidGitRepositoryError and falls back to the CLI path. Keep the guard; suppress with # pyright: ignore[reportUnnecessaryComparison].

src/frameset_tools

  • A src/frameset_tools/frameset_display.py:105grid_size: tuple[int, int] is required (non-Optional) in __init__, so if grid_size is not None else entries is unreachable. Drop the conditional, or change the parameter type to tuple[int, int] | None = None if callers really do want the fallback.

src/hyperstar

  • B src/hyperstar/accelerate_patches.py:51 — canonical single-assign init flag _PATCHED; annotate as _PATCHED: bool = False (not a true constant).
  • B src/hyperstar/autopilot/eval_surrogate.py:108LossCurveFeatures.convergence_pct: float is too narrow; widen to float | None since callers filter against None.
  • B src/hyperstar/autopilot/eval_surrogate.py:226_X is an instance list attribute, not a constant; rename to _x_features or annotate without uppercase.
  • B src/hyperstar/experiment_gui/transform.py:35 — try/except import idiom; annotate HAS_DATEUTIL: bool = False (not a constant; both branches assign once).
  • B src/hyperstar/experiments/_registry.py:66_DISCOVERED is a single-assign init flag; annotate as _DISCOVERED: bool = False.
  • B src/hyperstar/experiments/lumi_remote_eval_smoke.py:33 — module-level conditional init of _REQUEST_TOPIC; annotate _REQUEST_TOPIC: str once or rename to non-uppercase.
  • B src/hyperstar/experiments/lumi_remote_eval_smoke.py:34 — same pattern: _RESULT_TOPIC conditionally initialized in two branches.
  • A src/hyperstar/hyperstar_loss_statistics.py:134get_robot_name returns str and raises on None; the is not None ternary is dead, simplify to robot_name_val or ''.
  • A src/hyperstar/hyperstar_loss_statistics.py:167 — same dead defensive: get_robot_name always returns str or raises; drop the val if val is not None else '' walrus pattern.
  • C src/hyperstar/hyperstar_loss_statistics.py:279source_df is typed pd.DataFrame but pandas-Any callers can pass None; keep guard, suppress with # pyright: ignore[reportUnnecessaryComparison].
  • A src/hyperstar/hyperstar_loss_statistics.py:507 — same get_robot_name pattern in lambda; the None branch is dead.
  • B src/hyperstar/hyperstar_trainer.py:380_optimizer: Optimizer declared non-Optional but property checks for None; widen field to Optimizer | None.
  • B src/hyperstar/hyperstar_trainer.py:700_training_loader: DataLoader declared non-Optional but is None check exists; widen field.
  • B src/hyperstar/init.py:450GFX_BACKEND_INITIALIZED is a single-assign init flag; annotate : bool = False.
  • B src/hyperstar/intent_editor/auto_intent.py:246JointTimeline.qpos: NDArray[np.float32] is too narrow; widen to NDArray[np.float32] | None since dataset can yield empty timelines.
  • B src/hyperstar/intent_editor/auto_intent.py:246 — same: JointTimeline.action should be NDArray[np.float32] | None.
  • B src/hyperstar/intent_editor/auto_intent.py:414 — same JointTimeline.qpos widening needed.
  • B src/hyperstar/intent_editor/auto_intent.py:414 — same JointTimeline.action widening needed.
  • B src/hyperstar/intent_editor/auto_intent.py:470window_predict_int returns NDArray[np.int32] non-Optional but caller treats None as a possibility; widen return.
  • B src/hyperstar/intent_editor/services/auto_trim_service.py:265joint_timeline.action typed non-Optional; widen JointTimeline.action.
  • B src/hyperstar/intent_editor/services/auto_trim_service.py:332 — same JointTimeline.action widening needed.
  • C src/hyperstar/lerobot_datacleaner/datacleaner.py:596gripper_spec.ctrl_index: int typed strict but config-loaded data may have None; keep guard, suppress.
  • B src/hyperstar/lerobot_support.py:73 — draccus-parsed arm_config.calibration_dir is typed str but is actually str | None from upstream; widen the upstream stub or cast/ignore at boundary.
  • C src/hyperstar/lerobot_support_code/episode_writer.py:196episode_data: dict[str, NDArray] typed non-Optional but caller dicts come from heterogeneous parquet/JSON; keep guard, suppress.
  • B src/hyperstar/lerobot_support_code/patch_lerobot.py:61_HYPERSTAR_TYPES_IMPORTED is a single-assign import-success flag; annotate : bool = False.
  • C src/hyperstar/lerobot_support_code/repair_dataset.py:129dataset_fix.episode_data_index from upstream LeRobot has stub dict[str, Tensor] but is actually nullable at runtime; keep hasattr+is not None guard, suppress.
  • B src/hyperstar/models/hyperflow.py:385_prepare_qpos_history_embed(qpos_history: Tensor) is too narrow; caller passes components.qpos_history: Tensor | None. Widen param.
  • B src/hyperstar/models/hyperflow.py:392 — same: _prepare_action_history_embed(action_history: Tensor) should be Tensor | None.
  • A src/hyperstar/models/hyperflow.py:501ip_scale is set unconditionally from compute_intent_progress_loss_scale; the is not None ternary is dead.
  • A src/hyperstar/models/hyperflow.py:545ip_scale_avg = ip_scale.mean(dim=-1) is unconditional; is not None check is dead.
  • A src/hyperstar/models/hyperflow.py:557 — same dead ip_scale is not None guard; remove.
  • B src/hyperstar/models/hyperformer.py:116nn.ModuleDict.__getitem__ returns Module non-Optional per stubs, but params['wte'] semantically may be a None placeholder; cast or use a typed lookup helper.
  • B src/hyperstar/models/hyperformer.py:118 — same params['drop'] is None against nn.ModuleDict stub limitation.
  • B src/hyperstar/models/hyperformer.py:120 — same params['ln_f'] is None against nn.ModuleDict stub limitation.
  • A src/hyperstar/models/hyperformer.py:278 — early return if boundary_bias is None: return self.bias at line 267 makes boundary_bias is not None at 278 unreachable; delete the redundant inner check.
  • B src/hyperstar/models/hyperformer_basic.py:216 — local script-style B = key_padding_mask.shape[0] shadows an outer B; rename to b (not a module constant).
  • A src/hyperstar/models/hyperstar_model.py:1267forward_state.diffusion_mask was already narrowed by assert ... is not None at line 966; the second is not None is dead.
  • B src/hyperstar/robot_data/dataloader.py:506_config: Config declared non-Optional but property defensively checks for None; widen field to Config | None.
  • B src/hyperstar/robot_data/dataloader.py:512 — same _usage: HyperstarDatasetUsage field needs | None.
  • B src/hyperstar/robot_data/dataloader.py:656seed: int = 42 is non-Optional but seed is not None check exists; widen param to int | None = None.
  • B src/hyperstar/robot_data/lerobot_dataset.py:665hf_dataset typed non-Optional via the assignment branches; widen attribute since the is None branch returns 0.
  • B src/hyperstar/robot_data/lerobot_dataset.py:738null_images from _create_null_images() -> NDArray non-Optional; widen return or remove the is not None check.
  • A src/hyperstar/robot_data/video_decoder/base_decoder.py:130frame is reassigned from get_fallback_frame() (returns Tensor) inside the prior if frame is None block, so frame is not None is always true; simplify.
  • B src/hyperstar/robot_data/video_decoder/pyav_decoder.py:188cv2.imdecode stubs say NDArray non-Optional but it returns None on decode failure; widen the stub locally or cast — the guard is correct.
  • A src/hyperstar/run.py:182result = (cuda_device_order, [...]) is a tuple literal that cannot be None; the is None check is genuinely dead.
  • A src/hyperstar/task_definitions.py:147RobotDefinition.robot_arms: list[str] typed non-Optional and code paths populate it; the is None check is dead.

src/image

  • B src/image/analysis/countcompartments.py:56rgb parameter is annotated NDArray but callers pass results of cv2 decode / frameset accessors that can yield None. Widen the parameter to NDArray | None.
  • B src/image/analysis/countcompartments.py:56 (col 23) — same site for depth; same fix.
  • B src/image/analysis/helper.py:22no_crate(depth: NDArray) defensively checks depth is None; cv2.imdecode can return None. Widen to depth: NDArray | None.
  • B src/image/analysis/pickpair.py:82rgb0/rgb1 annotated NDArray but the function is explicitly designed to handle missing channels (locals are pre-initialized to None). Widen the four parameters in diff_images(...) to NDArray | None.
  • B src/image/analysis/pickpair.py:82 (col 29) — same site, rgb1 clause; same fix.
  • B src/image/analysis/pickpair.py:85 — same function, depth0 clause; same fix.
  • B src/image/analysis/pickpair.py:85 (col 31) — same function, depth1 clause; same fix.
  • B src/image/analysis/pickpair.py:88[depth, color, structural, luminance] filter if channel is not None. Once parameters are widened to | None, this becomes valid.
  • B src/image/utils.py:248cv2.imdecode truly returns Optional[ndarray] at runtime; cv2 stub is too narrow. Either install/upgrade cv2 stubs, or annotate decoded: NDArray | None = cv2.imdecode(...). The is None guard must stay.

src/infra

  • C src/infra/google/bigquery.py:258bq_client_query(..., raise_on_error=False) is typed RowIterator | _EmptyRowIterator per google-cloud-bigquery stubs, but with raise_on_error=False the wrapper may legitimately swallow errors and return None at runtime. Keep the defensive check; add # pyright: ignore[reportUnnecessaryComparison]. (Could alternatively be A if the wrapper provably never returns None — verify the wrapper's real return type before fixing.)
  • B src/infra/google/load_bq_from_excel.py:64string_fields is locally rebound to string_fields or [] on line 36, so it's list[Unknown] at this point. Either drop the redundant check, or change the parameter annotation to list[str] | None and remove the or [] coercion.
  • C src/infra/google/metadataservice.py:109bq_client_query returns RowIterator | _EmptyRowIterator per stubs, but this code clearly expects None to be possible (logs "did not contain barcode"). Keep the defensive guard; add # pyright: ignore[reportUnnecessaryComparison].
  • A src/infra/google/metadataservice.py:150barcode was just reassigned from clean_barcode (None-checked at line 145 and returns early). At line 150 barcode: str cannot be None. Drop the if barcode is not None else [].
  • A src/infra/google/reporting/mail.py:81service_account.Credentials.with_subject is typed as returning Credentials (non-Optional) per google-auth stubs. The if delegated_credentials is None branch is dead.
  • A src/infra/google/reporting/mail.py:88 — same case for with_scopes; dead branch.
  • C src/infra/slack/main.py:219bq_client_query returns non-Optional per stubs, but the [] if result is None else list(result) pattern is a one-liner safety net for the wrapper's real behavior. Keep + # pyright: ignore[reportUnnecessaryComparison].
  • A src/infra/tracing.py:63print_result is typed bool | Logger (default False); it cannot be None. Drop the False if print_result is None else print_result line entirely.

src/pap_rob

  • A src/pap_rob/calibration_handler.py:48pt is list[NDArray | None]; the list itself is never None. Drop the pt is None clause; keep pt[0] is None.
  • A src/pap_rob/calibration_handler.py:406assert fs is not None two lines above already proves it; drop the redundant if fs is not None and unindent.
  • A src/pap_rob/heuristical_pick_finder.py:181margins: list[float] is non-Optional and always initialized to []; drop the if self.margins is not None ternary.
  • C src/pap_rob/heuristical_pick_finder.py:357 — model output dataclass confidence: float could be missing in JSON-deserialized inference; suppress with # pyright: ignore[reportUnnecessaryComparison].
  • B src/pap_rob/lessons/nowaste_pick_and_place_lesson.py:238world.robot_controller is non-Optional in __init__ but lesson teardown can run before init completes; widen to RobotController | None.
  • B src/pap_rob/lessons/robot_calibration_lesson.py:102 — same as above.
  • B src/pap_rob/lessons/single_pick_lesson.py:76 — same as above.
  • A src/pap_rob/pap_rob_api.py:120_running_mode = 'manual-picker' module-level constant, never reassigned; drop the is not None guard.
  • A src/pap_rob/pap_rob_api.py:165FrameSet.color_frames is a @property returning list[Frame], never None; drop or fs.color_frames is None.
  • A src/pap_rob/pap_rob_api.py:176depth_frames property returns list[Frame]; replace with truthiness if not frameset.depth_frames.
  • A src/pap_rob/pap_rob_api.py:200 — same as above; drop or frameset.depth_frames is None.
  • B src/pap_rob/pap_rob_api.py:206world.get_station() raises on missing rather than returning None; widen its return to Station | None and remove the raise.
  • B src/pap_rob/pap_rob_api.py:212Camera.get_latest_frameset() annotated FrameSet but subclasses can return None at runtime; widen to FrameSet | None.
  • A src/pap_rob/pap_rob_api.py:217station.cameras: list[CalibratedCamera] so intrinsics is non-Optional; drop the check.
  • A src/pap_rob/pap_rob_api.py:217 — same: extrinsics is CameraExtrinsics on CalibratedCamera.
  • C src/pap_rob/pap_rob_api.py:313cast(dict, request.get_json(silent=True)) hides that flask json can be None; suppress.
  • C src/pap_rob/pap_rob_api.py:314 — same JSON cast pattern; suppress.
  • C src/pap_rob/pap_rob_api.py:356cast(RobotCellStatus, query_params.get('point', 'not-set')) widens default literal beyond the Literal type; suppress.
  • C src/pap_rob/pap_rob_api.py:620cast(dict, request.get_json(silent=True)) hides runtime None; suppress.
  • B src/pap_rob/pap_rob_api.py:696Camera.get_latest_frameset() annotation too narrow; widen to FrameSet | None.
  • B src/pap_rob/pap_rob_world.py:156bqtf: BQToFile parameter is non-Optional but if bqtf is not None ternary on next line proves caller can pass None; widen parameter.
  • B src/pap_rob/pick_frameset_storage.py:130Station.top_roi is Crop (sentinel Crop.NONE); the None-check suggests Optional intent; widen field to Crop | None.
  • A src/pap_rob/pick_instruction_provider.py:430assert operator_input.recommended_pick_point is not None at line 412 narrows it; drop p1 is not None and.
  • B src/pap_rob/pickjob.py:197 — comment says "Might have already cancelled this picking job"; widen rjob parameter to RobotPickJob | None.
  • A src/pap_rob/pickjob.py:326done_robot_jobs: list[RobotPickJob] elements are never None; drop if rj is not None.
  • B src/pap_rob/place_env.py:115Station.min_camera_depth is set to 10.0 default but legacy stations may not have it; widen to float | None.
  • A src/pap_rob/placement/placement_provider.py:253assert context.pick_env.used_tool is not None at line 222 narrows it; drop if tool is not None.
  • A src/pap_rob/placement/virtual_placement_image_source.py:47 — line 42 already returns when finalizing_robot_job is None, so reassigned rjob is non-None; drop.
  • B src/pap_rob/station.py:139 — TODO comment explicitly notes the extrinsics type hint is wrong; fix CalibratedCamera.extrinsics annotation back to CameraExtrinsics | None.
  • C src/pap_rob/wms_interaction_api.py:130cast(dict, request.json) hides runtime None body; suppress.
  • C src/pap_rob/wms_interaction_api.py:257 — same JSON cast pattern; suppress.
  • C src/pap_rob/wms_interaction_api.py:297cast(WMSPickJobStatus, ...) hides that get_pickjob_status returns WMSPickJobStatus | None; suppress (or remove the cast).
  • A src/pap_rob/wms_interaction_api.py:328abort_pickjob_requested_by_wms returns bool, never None; drop the if res is None branch (it's confused with truthiness — change to if not res).
  • C src/pap_rob/wms_interaction_api.py:334 — JSON cast pattern; suppress.
  • C src/pap_rob/wms_interaction_api.py:351 — JSON cast pattern; suppress.
  • C src/pap_rob/wms_interaction_api.py:366 — JSON cast pattern; suppress.
  • C src/pap_rob/wms_interaction_api.py:381 — JSON cast pattern; suppress.
  • B src/pap_rob/wms_job_manager.py:283PickJob.wms_job: WMSPickJob non-Optional but the field guards suggest in-flight reset paths; widen to WMSPickJob | None.
  • B src/pap_rob/wms_job_manager.py:307 — same as above.

src/robots

  • C src/robots/core/robot_controller.py:337wms_job is None defends a queue tuple-unpack against unexpected None payloads. Producer types it as Orderline, but keeping a runtime guard on cross-thread queue contents is sensible; add # pyright: ignore[reportUnnecessaryComparison].
  • B src/robots/impl/hkm/opcua/opcua.py:473_callback is annotated Callable[..., None] (always set in __init__ via callback or opcua_client_value_changed), so the truthy guard is unreachable. Either drop the guard or annotate _callback as Callable[..., None] | None.
  • B src/robots/impl/hkm/opcua/opcua.py:706opcua_config.password is not None runs against OpcuaConfig.password: str (no Optional), but config loading may legitimately leave it unset for simulated servers. Widen to str | None.
  • B src/robots/impl/hkm/opcua/opcua_client.py:162 — Same _callback pattern as opcua.py:473.
  • B src/robots/impl/hkm/opcua/opcua_recorder.py:351_flush_change_queue returns tuple[list, tuple[datetime, datetime]] | None; after the outer None check the inner timespan is None is unreachable per signature. Widen the return type.
  • C src/robots/impl/hkm/opcua/opcua_recorder.py:500dc.server_timestamp typed as datetime but originates from monitored_item.Value.ServerTimestamp over the OPCUA wire (can be missing). Keep guard, suppress.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:157 — Classic PLC type-dispatch: plc_datatype is either a ctypes class or a tuple (nested struct def). Widen annotation.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:219 — Same type(plc_datatype) is tuple PLC dispatch.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:257 — Same PLC dispatch pattern.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:310 — Same PLC dispatch pattern.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:381 — Same PLC dispatch pattern.
  • B src/robots/impl/kuka/ads/network/pyads_bugfixes.py:434 — Same PLC dispatch pattern.
  • B src/robots/impl/kuka/kuka_robot_sim.py:317self._handler.get_variable_by_name(var_path) is from an external pyads handler whose stub omits the legitimate "not found" case. The same method is checked truthy at line 342, confirming runtime can return falsy/None. Widen the handler return type.

src/simulation

  • A src/simulation/mujoco_sim/robotics.py:595camera_from is unpacked from camera (typed str | list[str]); after the if/else either branch assigns a str. The trailing and camera_from is not None is unreachable.
  • B src/simulation/roboception.py:599RoboceptionData.camera_images: dict[str, NDArray[np.uint8]], but the dict is populated from observation.camera_frames and recording can omit a camera (yielding None per camera). Widen to dict[str, NDArray[np.uint8] | None].
  • A src/simulation/sim_service/grpc_robot_cell_pool.py:106code = _rpc_status_code(err) returns StatusCode | None, but line 99 already raises when code not in _RETRYABLE_STATUS_CODES (which includes None), so by line 106 code is provably non-None. The if code is not None else 'unknown' fallback is dead.

src/synq_api

  • B src/synq_api/main.py:558ProductInfo.barcode is annotated str, but the value originates from parsed Synq HTML/JSON and is missing in practice (the branch logs "No barcode found" and reports a deviation). Widen to str | None.
  • B src/synq_api/session.py:53parsers.parse_for_workstationurl annotated -> str but parses scraped HTML and can fail to find a match at runtime; the explicit raise here is the recovery path. Widen the parser return type to str | None.
  • B src/synq_api/sics_api_calls.py:55SICS_API_IP is an all-caps module-level mutable host config, not a Final. Annotate SICS_API_IP: str = ... (non-Final) or rename to lowercase / move into a small config object.
  • B src/synq_api/sics_api_calls.py:55 — Same as above for SICS_API_PORT.
  • B src/synq_api/sics_api_calls.py:113LAST_PREANNOUNCE_DICT is all-caps but is a mutable test/sim cache. Rename or annotate non-Final.

src/utils

  • B src/utils/config/config.py:163Config._app_name: str is annotated as str, but has_app_name() exists precisely because callers may construct Config without one. Widen to _app_name: str | None.
  • B src/utils/eventloop.py:42_event_loop is initialized to None in __new__ (singleton-with-lazy-init pattern), but the attribute has no annotation so pyright infers AbstractEventLoop from the later assignment. Add an explicit _event_loop: AbstractEventLoop | None.
  • A src/utils/memory.py:220base starts as the (non-None) input ndarray and is only reassigned to base.base while that value isinstance(_, np.ndarray), so base is provably an ndarray after the loop. The if base is not None else 0 tail is dead.
  • A src/utils/memory.py:238 — Same loop pattern as above.

src/viz_server

  • B src/viz_server/product_classification.py:107request.json (Flask) is Any | None upstream but cast(dict, request.json) strips the None from the static view. Drop the cast(dict, ...) so the inferred type is Any | None and the guard is well-typed; alternatively # pyright: ignore[reportUnnecessaryComparison].

tests/integration

  • C tests/integration/data_engine/data_engine_db_test.py:704bq_client.query().result() returns RowIterator | _EmptyRowIterator per BigQuery stubs, never None. Defensive guard against real BQ client behavior; keep, add # pyright: ignore[reportUnnecessaryComparison].
  • B tests/integration/hyperstar/helpers/history_usage_helpers.py:960 — function signature is tensor: Tensor but body explicitly handles the None case (logs "None (no history)"). Change parameter to tensor: Tensor | None.
  • C tests/integration/hyperstar/joint_discretization/discretizer_test.py:241dataset.task_definition.name typed as str; defensive null-guard. Real datasets may have empty/optional task names; keep, suppress.
  • B tests/integration/hyperstar/model_integration/multi_iteration_sampling_test.py:97 — local literal assignment narrows sampling_iter/action_predict_auto_steps to Literal. Annotate as int and int | None.
  • B tests/integration/hyperstar/model_integration/multi_iteration_sampling_test.py:110 (col 8) — sampling_iter = 1 narrows to Literal[1]; annotate as int.
  • B tests/integration/hyperstar/model_integration/multi_iteration_sampling_test.py:110 (col 31) — action_predict_auto_steps = 2 narrows; annotate as int | None.
  • B tests/integration/hyperstar/model_integration/multi_iteration_sampling_test.py:239 — tuple-unpacked loop var narrows; type the test_cases list as list[tuple[int, int | None]].
  • B tests/integration/hyperstar/model_integration/multi_iteration_sampling_test.py:264 — same literal-narrowing pattern.
  • B tests/integration/hyperstar/model_integration/training_prefill_test.py:100training_prefill_mode = 5 narrows to Literal[5]; production type is int | Literal['none'].
  • B tests/integration/hyperstar/model_integration/training_prefill_test.py:149 (col 24) — sampling_iter = 1 narrows; annotate as int.
  • B tests/integration/hyperstar/model_integration/training_prefill_test.py:149 (col 47) — training_prefill_mode = 5 narrows; annotate as int | Literal['none'].
  • B tests/integration/hyperstar/model_integration/training_prefill_test.py:184 — same; annotate as int | Literal['none'].
  • B tests/integration/hyperstar/model_integration/training_prefill_test.py:220training_prefill_mode = prefill_count narrows to int; annotate as int | Literal['none'].

tests/unit

  • C tests/unit/hyperstar/joint_discretization/discretizer_test.py:183dataset.task_definition.name typed as str; defensive null-guard mirroring the integration test. Keep, suppress.
  • B tests/unit/hyperstar/models/low_confidence_remasking_test.py:192last_hf = torch.randn(2, 10, 32) narrows to Tensor; the test deliberately mirrors production last_hf is not None check. Annotate as last_hf: Tensor | None = torch.randn(...).
  • B tests/unit/image/transformers/hue_transformer_test.py:57 (col 5) — C reassigned at line 57 after first being bound at line 47 (semantically swaps axis ordering — latent code smell). Rename to lowercase c, h, w.
  • B tests/unit/image/transformers/hue_transformer_test.py:57 (col 8) — same for H.
  • B tests/unit/image/transformers/hue_transformer_test.py:57 (col 11) — same for W.

What this means for follow-up work

Of the 53 (A) sites, the largest concentrations are:

  • src/hyperstar/models/hyperflow.py — 3 dead ip_scale is not None ternaries (single fix).
  • src/hyperstar/hyperstar_loss_statistics.py — 3 dead get_robot_name ternaries (single fix).
  • src/data_engine/processing/batch_processor.py — 4 dead checks downstream of the same assert frameset_row (one cluster).
  • src/pap_rob/pap_rob_api.py — 6 dead checks against @property returns and module-level constants.
  • src/camera/femtomega/* — 3 dead next_frameset() None-checks, plus 1 redundant _femtomega re-check.
  • Various individual already-narrowed-by-assert sites scattered across src/pap_rob and src/hyperstar.

Of the 111 (B) sites, the work consolidates into a small number of dataclass / signature widenings (see "High-leverage fixes" above). A concentrated 1–2-day effort on these clusters would resolve most of the (B) cohort.

Of the 23 (C) sites, 18 are either Flask request.json casts in pap_rob (one typed helper fix would handle 8 of them) or the bq_client_query wrapper return-type ambiguity (3 sites). Tactical # pyright: ignore with comments is the right call until the underlying wrappers/casts are reworked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment