Problem
During #977 (cudf SIGSEGV fix), a real UnboundLocalError in hop.py was missed by both ruff and mypy:
# edge_map assigned only inside this branch:
if g_out._edges is not None and edge_hop_col is not None and ...:
edge_map = ...
# then used outside it — NameError/UnboundLocalError at runtime:
if missing_mask.any() and edge_hop_col is not None and ...:
... safe_map_series(..., edge_map) # edge_map possibly unbound!
The bug was masked by an upstream SIGSEGV (so it never surfaced in production), and only caught by writing a test that exercised the specific branch combination (`label_node_hops=True, label_edge_hops=None` with a non-empty `missing_mask`).
Why ruff/mypy missed it
- ruff F821 only catches names with no assignment anywhere in scope — not conditionally-assigned names
- mypy does not flag "possibly unbound" from conditional branches unless the variable is annotated before the branch (e.g. `edge_map: pd.Series` declared before the `if`)
- Neither tool does full dataflow / reachability analysis for this pattern
What would catch it
pyright (default mode, not even strict) detects "possibly unbound" variables from conditional assignments and flags them at the use site. This is a well-known difference from mypy.
Proposal
Explore adding pyright in monitor / informational mode to CI as a sequence of stacked PRs:
- Observability
- Run pyright on the `graphistry/` source (excluding tests, matching mypy.ini scope)
- Enforce in CI any rules that already pass
- Initially non-blocking (warnings only) for rest to assess the volume of pre-existing issues
- Clear reporting step in CI for easy auditing
- Initial refactor
- Create and implement on a metaissue for initial target of rules that the repo can enforce
- There should be a small number of changes that go a long way
- Promote these to blocking, and leave the rest as warning
- For local dev flow, add pyright to the mypy steps as a default-on option
- Follow-on refactors
- Track a metaissue prioritized rules that fail
- Sequence of PRs that fix each one
This does not replace mypy — they catch different things. Pyright's dataflow analysis is complementary.
References
Problem
During #977 (cudf SIGSEGV fix), a real
UnboundLocalErrorinhop.pywas missed by both ruff and mypy:The bug was masked by an upstream SIGSEGV (so it never surfaced in production), and only caught by writing a test that exercised the specific branch combination (`label_node_hops=True, label_edge_hops=None` with a non-empty `missing_mask`).
Why ruff/mypy missed it
What would catch it
pyright (default mode, not even strict) detects "possibly unbound" variables from conditional assignments and flags them at the use site. This is a well-known difference from mypy.
Proposal
Explore adding pyright in monitor / informational mode to CI as a sequence of stacked PRs:
This does not replace mypy — they catch different things. Pyright's dataflow analysis is complementary.
References