Skip to content

FIX: align draw's Python size dispatch with the jitted path (#918) - #955

Open
mmcky wants to merge 1 commit into
mainfrom
fix/draw-numpy-integer-size-918
Open

mmcky wants to merge 1 commit into
mainfrom
fix/draw-numpy-integer-size-918

Conversation

@mmcky

@mmcky mmcky commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Aligns the Python size dispatch in quantecon.random.draw with the jitted path. Replaces #919, which was closed with thanks; this is the reduced form of the same fix.

The bug

draw decides between "an array of size draws" and "one scalar draw" in two places, with two tests that do not agree:

Path Test
Pure Python body isinstance(size, int)
@overload implementation isinstance(size, types.Integer)

A NumPy integer is not a Python int but is a types.Integer, so the same call returns different things depending on whether the call site is jit-compiled — and nothing warns. This is easy to hit: size arrives as np.int64 from any ordinary numpy expression (counts[i], arr.sum(), arr.argmax(), a // on an array), while len(x) and arr.shape[0] give Python ints and are fine, which is why it survives casual testing.

The change

One line in the Python body, plus the comment explaining it:

if isinstance(size, (int, np.integer)) and not isinstance(size, bool):

The bool exclusion is required in the other direction: bool subclasses int in Python, but Numba types it as Boolean, so without it the two paths would still disagree for size=True.

The @overload needs no change. types.Boolean is not a subclass of types.Integer, so the jitted path already dispatches every case correctly — only the Python body was wrong.

Verified

Dispatch table on this branch, Python against jitted, for every case in #918:

size Python Jitted
10 array(10,) array(10,) agree
np.int64(10) array(10,) array(10,) agree
np.int32(10) array(10,) array(10,) agree
np.uint8(10) array(10,) array(10,) agree
True scalar scalar agree
np.bool_(True) scalar scalar agree
10.0 scalar scalar agree
None scalar scalar agree

None was checked across all four call shapes — omitted and explicit, from Python and from a jitted caller — since _is_no_rng shows Numba spells "no value" more than one way.

  • Full suite: 679 passed
  • flake8 --select=F401,F405,E231 quantecon: clean
  • git diff --check: clean

Release note needed

draw(cdf, np.int64(10)) now returns ten draws from Python where it previously returned one scalar. That is what the caller asked for, and what jitted callers already received, but it is a silent behaviour change and should be called out in the 0.12.0 release notes. Per AGENTS.md there is no per-PR changelog entry to add.

Closes #918. Replaces #919.

🤖 Generated with Claude Code

`quantecon.random.draw` chose between an array of draws and a single
scalar draw with `isinstance(size, int)` in the Python body and
`isinstance(size, types.Integer)` in the `@overload`. A NumPy integer is
not a Python `int` but is a `types.Integer`, so `draw(cdf, np.int64(10))`
returned one scalar from Python and ten draws from a jitted caller, with
no error either way. `bool` is the reverse case: it passes
`isinstance(x, int)` but Numba types it as `Boolean`.

Widening the Python predicate to `(int, np.integer)` and excluding `bool`
makes the two paths agree on every input in #918's table, verified
including `None` across all four call shapes.

The `@overload` needs no change: `types.Boolean` is not a subclass of
`types.Integer`, so the jitted path already dispatches every case
correctly.

Behaviour change: `draw(cdf, np.int64(10))` now returns ten draws from
Python where it previously returned one scalar. That is what the caller
asked for and what jitted callers already received, but it is silent and
needs a line in the 0.12.0 release notes.

Closes #918. Replaces #919.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 9, 2026 06:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A regression test for the size=True / np.bool_(True) scalar-dispatch case is still missing even though the change explicitly addresses that edge case.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes an inconsistency in quantecon.random.draw where the pure-Python implementation treated size=np.int64(10) as a scalar draw, while the Numba-jitted overload treated it as an array-sized draw, leading to divergent return types depending on whether the caller was jit-compiled.

Changes:

  • Update the pure-Python draw size-dispatch predicate to accept NumPy integer scalars (and explicitly exclude bool) so it matches the @overload dispatch behavior.
  • Add a regression test asserting NumPy-integer size requests an array return for both Python and jitted call paths.
File summaries
File Description
quantecon/random/utilities.py Align pure-Python size dispatch with the Numba overload by accepting np.integer (excluding bool).
quantecon/random/tests/test_utilities.py Add regression coverage ensuring NumPy integer size returns an array on both call paths.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +128 to +138
def test_numpy_integer_size(self):
"""
A numpy integer `size` must request an array, as a Python `int`
does and as the jitted path already did. See #918.

"""
size = np.int64(10)
for func in self.draw_funcs:
out = func(self.cdf, size)
assert_(out.shape == (size,))

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 90.884%. remained the same — fix/draw-numpy-integer-size-918 into main

@oyamad oyamad left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my thought: I wouldn't make (the Python version of) draw(cdf, size=True) return any value. As I stated in #917 (comment), we should only fix the bug I had introduced, keeping the behavior of the Python version "unspecified" or "unsupported" (the @overload is the contract). And thus I wouldn't add any new test.

# `bool` subclasses `int` in Python but Numba types it as `Boolean`,
# not `Integer`, so the exclusion keeps this branch in step with the
# `@overload` implementation below. See #918.
if isinstance(size, (int, np.integer)) and not isinstance(size, bool):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if isinstance(size, (int, np.integer)) and not isinstance(size, bool):
if isinstance(size, (int, np.integer)):

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

random.draw: size dispatch diverges between the Python and jitted paths for numpy integers

4 participants