docs: add optype.numpy.ctypeslib reference page - #581
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new reference page documenting the optype.numpy.ctypeslib module, which provides typed ctypes constructor reexports and abstract typing aliases for NumPy scalar-like C types (addressing the request in #448 to document this niche-but-existing API surface).
Changes:
- Introduces
docs/reference/numpy/ctypeslib.mddescribing the reexportedctypesconstructors exposed byoptype.numpy.ctypeslib. - Documents the module’s higher-level abstract typing aliases (
CType,CScalar,Array, numeric families, etc.).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@jorenham could you please re-review request from copilot, so that we can check if anything is missing. Thanks! |
jorenham
left a comment
There was a problem hiding this comment.
The navigation link is missing (see zensical.toml)
|
thanks for the review, this needs lot improvement, i'll take some time for this and update (maybe on sunday) |
|
hoping did not miss anything, i'll try to verify once again from my side, as i think this is going to create pain for you to review 🙏 . |
Signed-off-by: Aniket <148300120+Aniketsy@users.noreply.github.com>
|
hi @jorenham could you please ask copilot to review once .. so that we can proceed with this... As i have asked claude(sonnet 4.6) to review and then improved . |
|
I cancelled my copilot subscription, but I'll have Claude Fable take a look 👌 |
jorenham
left a comment
There was a problem hiding this comment.
Ok I had Claude Fable 5 review this, and here's what it has to say:
Reviewed against the source (optype/numpy/ctypeslib.py, _ctypeslib.pyi) and the earlier review rounds.
Verified
- All previous feedback is addressed: nav entries in
zensical.tomlanddocs/reference/index.md, one row per alias, abstract aliases spelled out as explicit unions, notes column removed,ct.used consistently. - The ILP32/LP64/LLP64 aliasing table is correct; I checked the
c_int/c_long/c_longlongandc_(s)size_tcollapsing rules againstctypesdirectly. - The union tables match the definitions in the source, including the
CScalar[int]invariance trick and the reasonBoolsits outsideNumber. - Both example snippets typecheck (basedpyright), and the first one runs as written.
Wording issue
- Abstract type aliases intro: "
_CDatahas no runtime counterpart" isn't quite right. The class exists at runtime (it'sc_int.mro()[2]); it just isn't importable by name fromctypes. And "These exist only for annotations" doesn't hold forCType/CScalar, which optype does export at runtime. Suggest rewording along the lines of "not importable fromctypesby name, and not meant to be instantiated".
Nits
- The admonition says
ObjectandBoolshadow builtins; they don't (the capitalization differs). The real collisions aretyping.Genericandoptype.numpy.Array, which means something different in the parent namespace. I'd trim the note to those two. - "There is no data model on which all four are distinct": the paragraph only names three types, so "four" is confusing.
- NumPy version notes: "the distinction is not observable in practice" overstates it;
c_void_pandc_size_tare distinct classes with different.valuesemantics. The source comment's "almost always equivalent" is the safer phrasing. - The heading "Several of these names are aliases of each other" reads as a sentence; something shorter like "Aliased names" would fit the page style better.
Out of scope, found while verifying
- The runtime values in
_ctypeslib.pyare swapped relative to the stub:_, CType, CScalar, *_ = ct.c_int.mro()yields runtimeCType = _SimpleCDataandCScalar = _CData, while the.pyideclares the opposite. The doc correctly follows the stub, but this deserves a separate fix.
Otherwise accurate and complete. LGTM after the wording fix.
Pretty decent review IMO, right? I, at least, agree with the robot here.
RE: the out of scope issues it found; I'll look into that myself.
|
Oh and one more thing; The page title looks a bit out-of-style in the NumPy menu:
So something like Also, maybe we should mention somewhere that this |
|
Thanks for the review, today i'll address these review points so that we can proceed |
|
well this comment is off topic 😀 I've read the blog post of nominating you as a member in Python - Typing ... excited for results and seeing you as nominated . atleast then could you please review my PRs there with light speed as there sometimes after being approved we don't know after how many months it get merged ( just kidding please ignore) please ping me once results are out if i miss that ... i'm waiting ( so that i can congratulate) 😊 Also yesterday, I've explored typestats dasboard and opened small PR in matplotlib to increase type coverage , just curious in some projects there is 0 % coverage . Will they be interested in improving this or there are any limitations ( which is stopping them ). |
They'll announce the results in that discourse thread, so you can follow it to get notified right at the source.
Some maintainers simply don't like Python typing (yet 😏 ), or prefer outsourcing it to typeshed or third party library |
| - **[UFunc](numpy/ufunc.md)**: Universal functions | ||
| - **[Type Aliases](numpy/aliases.md)**: Common type aliases | ||
| - **[Low-level](numpy/low-level.md)**: Low-level NumPy interfaces | ||
| - **[ctypeslib](numpy/ctypeslib.md)**: Typed `ctypes` aliases for NumPy scalar-like C types |
There was a problem hiding this comment.
That's better, but it's a bit too long for a menu entry
There was a problem hiding this comment.
I've updated with shorter version , let me know your thoughts
Signed-off-by: Aniket <148300120+Aniketsy@users.noreply.github.com>
jorenham
left a comment
There was a problem hiding this comment.
I had some time and gave this another look.
One general thing: The table markup is very long now; could you format it like in e.g. docs/reference/core/attributes.md?
| `optype.numpy.ctypeslib` re-exports standard-library `ctypes` constructors under | ||
| names that mirror NumPy's scalar naming — `Int32` for the `ctypes` counterpart of | ||
| `np.int32`, `LongDouble` for `np.longdouble`, and so on — and adds abstract type | ||
| aliases mirroring NumPy's scalar hierarchy (`SignedInteger`, `Inexact`, | ||
| `Generic`, …) for use in annotations. |
There was a problem hiding this comment.
this bit doens't really add much, and is alreayd explained below in fewer words
| Unlike NumPy, `ctypes` has no distinct fixed-width types. `c_int8` *is* | ||
| `c_byte`, `c_int16` *is* `c_short`, and the rest are resolved by `ctypes` at | ||
| import time from the platform's type sizes. A type checker cannot distinguish | ||
| names that resolve to the same class: |
There was a problem hiding this comment.
This isn't right, and also not really relevant here because we're just re-exporting types from ctypes
| <table> | ||
| <tr><th align="left">alias</th><th align="left"><code>ILP32</code></th><th align="left"><code>LP64</code></th><th align="left"><code>LLP64</code></th></tr> | ||
| <tr><td align="left"><code>Int8</code></td><td align="left"><code>Byte</code></td><td align="left"><code>Byte</code></td><td align="left"><code>Byte</code></td></tr> | ||
| <tr><td align="left"><code>UInt8</code></td><td align="left"><code>UByte</code></td><td align="left"><code>UByte</code></td><td align="left"><code>UByte</code></td></tr> | ||
| <tr><td align="left"><code>Int16</code></td><td align="left"><code>Short</code></td><td align="left"><code>Short</code></td><td align="left"><code>Short</code></td></tr> | ||
| <tr><td align="left"><code>UInt16</code></td><td align="left"><code>UShort</code></td><td align="left"><code>UShort</code></td><td align="left"><code>UShort</code></td></tr> | ||
| <tr><td align="left"><code>Int32</code></td><td align="left"><code>IntC</code> = <code>Long</code></td><td align="left"><code>IntC</code></td><td align="left"><code>IntC</code> = <code>Long</code></td></tr> | ||
| <tr><td align="left"><code>UInt32</code></td><td align="left"><code>UIntC</code> = <code>ULong</code></td><td align="left"><code>UIntC</code></td><td align="left"><code>UIntC</code> = <code>ULong</code></td></tr> | ||
| <tr><td align="left"><code>Int64</code></td><td align="left"><code>LongLong</code></td><td align="left"><code>Long</code> = <code>LongLong</code></td><td align="left"><code>LongLong</code></td></tr> | ||
| <tr><td align="left"><code>UInt64</code></td><td align="left"><code>ULongLong</code></td><td align="left"><code>ULong</code> = <code>ULongLong</code></td><td align="left"><code>ULongLong</code></td></tr> | ||
| <tr><td align="left"><code>IntP</code></td><td align="left"><code>IntC</code> = <code>Long</code></td><td align="left"><code>Long</code> = <code>LongLong</code></td><td align="left"><code>LongLong</code></td></tr> | ||
| <tr><td align="left"><code>UIntP</code></td><td align="left"><code>UIntC</code> = <code>ULong</code></td><td align="left"><code>ULong</code> = <code>ULongLong</code></td><td align="left"><code>ULongLong</code></td></tr> | ||
| </table> |
There was a problem hiding this comment.
This suggests that the optype.numpy.ctypeslib types differ depending on the architecture, which isn't the case. So let's not make these docs a wiki on how types work in C, and linearly tabulate the exports.
| `ctypes` aliases `c_int` to `c_long` when the two have equal size, and | ||
| `c_longlong` to `c_long` likewise. So on `LP64`, `Long` **is** `LongLong`; on | ||
| `ILP32` and `LLP64`, `IntC` **is** `Long`. There is no data model on which all | ||
| three are distinct. | ||
|
|
||
| Annotating with either name of a pair is equally correct. The pairs exist so | ||
| code can be written in whichever vocabulary — NumPy's or C's — reads better at | ||
| the call site. |
There was a problem hiding this comment.
Following the same reasoning as above, this can go
| ### `LongDouble` | ||
|
|
||
| `ctypes` collapses `c_longdouble` into `c_double` whenever the two have the same | ||
| size, so `LongDouble` *is* `Float64` on MSVC and on arm64 macOS. | ||
|
|
||
| `c_longdouble` also works only as a *type*, not as a value: its `.value` is a | ||
| Python `float`, i.e. a C `double`, so it cannot carry the extra precision of an | ||
| 80- or 128-bit long double. Use it in `argtypes`/`restype` and in annotations; | ||
| don't use it to hold values. |
There was a problem hiding this comment.
This is about C types, not about optype.numpy.ctypeslib
| counterpart and are deliberately absent. `c_wchar` is likewise not exposed, | ||
| since NumPy maps no dtype onto it. | ||
|
|
||
| `Bytes` maps `np.bytes_` onto `c_char`, following |
There was a problem hiding this comment.
There's no mapping; it's just a re-export under a different name
| `np.long` and `np.ulong` are the NumPy 2.0 names for the C `long` types, and do | ||
| not exist under that spelling on `numpy < 2`. | ||
|
|
||
| ### Types with no `ctypes` equivalent |
There was a problem hiding this comment.
Let's call this something like "Differences between numpy and ctypes", because it also talks about differences between c_char and np.bytes_
|
|
||
| `CType` and `CScalar` correspond to the private `ctypes` base classes that every | ||
| C type derives from. They exist at runtime, but neither is importable from | ||
| `ctypes` by name, and neither is meant to be instantiated — use them in |
There was a problem hiding this comment.
Let's not use non-ascii characters like this emdash here
| ### How the numeric aliases are defined | ||
|
|
||
| `Integer`, `Floating`, and `ComplexFloating` are not literally spelled as the | ||
| unions above. They are defined as `CScalar[int]`, `CScalar[float]`, and | ||
| `CScalar[complex]`, and `Inexact`/`Number` are built from those. | ||
|
|
||
| `ct._SimpleCData` is **invariant** in its type parameter, so `CScalar[int]` | ||
| resolves to exactly the C integer types listed above — and, unlike a literal | ||
| union, also admits any third-party `_SimpleCData[int]` subclass. The unions in | ||
| the table describe what these aliases match among the types in this module. | ||
|
|
||
| Invariance is also why `Bool` is listed separately in `Generic`: `c_bool` is a | ||
| `_SimpleCData[bool]`, and `bool` is not `int` under invariance, so `Bool` is not | ||
| covered by `Number`. |
There was a problem hiding this comment.
This can seems like a lot of text to explain that "CScalar" is invariant; it's safe to assume that users know what invariance is, so let's skip the tutorial.
| ## Example | ||
|
|
||
| ```python | ||
| import ctypes as ct | ||
|
|
||
| import numpy as np | ||
| import optype.numpy.ctypeslib as opct | ||
|
|
||
| # the concrete names are the `ctypes` types themselves | ||
| assert opct.Int32 is ct.c_int32 | ||
|
|
||
| buf = (opct.Int32 * 4)(1, 2, 3, 4) | ||
| arr = np.ctypeslib.as_array(buf) | ||
| assert arr.dtype == np.int32 | ||
| ``` | ||
|
|
||
| `ctypes` converts simple return types to Python objects on call, so these names | ||
| belong on `argtypes`/`restype`, not on the Python-side return annotation: | ||
|
|
||
| ```python | ||
| lib = ct.CDLL("libsum.so") | ||
|
|
||
| # int64_t sum_i32(const int32_t *values, int n) | ||
| lib.sum_i32.argtypes = [ct.POINTER(opct.Int32), opct.IntC] | ||
| lib.sum_i32.restype = opct.Int64 | ||
|
|
||
|
|
||
| def zeroed[T: opct.Number](ctype: type[T], n: int) -> opct.Array[T]: | ||
| return (ctype * n)() |
There was a problem hiding this comment.
These examples are more about ctypes than it is about numpy-related static typing. So let's just remove this and keep it focused.
Remember that these are reference docs, not a user guide.
Thanks for giving time and reviewing this
make sense, i'll update 👍 |

fixes #448