diff --git a/docs/reference/index.md b/docs/reference/index.md
index e6303eba..0b3a554f 100644
--- a/docs/reference/index.md
+++ b/docs/reference/index.md
@@ -131,6 +131,7 @@ NumPy-specific typing utilities (requires NumPy):
- **[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
## Type Variance Notation
diff --git a/docs/reference/numpy/ctypeslib.md b/docs/reference/numpy/ctypeslib.md
new file mode 100644
index 00000000..13610d2d
--- /dev/null
+++ b/docs/reference/numpy/ctypeslib.md
@@ -0,0 +1,228 @@
+# `ctypeslib` submodule
+
+Typed `ctypes` aliases for NumPy scalar-like C types.
+
+This module is named after [`numpy.ctypeslib`][np-ctypeslib], and follows the
+dtype-to-`ctypes` correspondence that
+[`np.ctypeslib.as_ctypes_type`][as_ctypes_type] implements.
+
+`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.
+
+The concrete names are plain re-exports: they *are* the `ctypes` types, so they
+can be instantiated and passed to `argtypes`/`restype` as usual. The abstract
+names are typing-only.
+
+Throughout this page, `ctypes` is imported as `ct`:
+
+```python
+import ctypes as ct
+```
+
+The module assumes a C99-compatible compiler, a 32- or 64-bit system, and an
+[`ILP32`, `LLP64`, or `LP64` data model][data-models]. If that isn't the case
+for your platform, please [open an issue][issues].
+
+[np-ctypeslib]: https://numpy.org/doc/stable/reference/routines.ctypeslib.html
+[as_ctypes_type]: https://numpy.org/doc/stable/reference/routines.ctypeslib.html#numpy.ctypeslib.as_ctypes_type
+[data-models]: https://en.cppreference.com/w/c/language/arithmetic_types
+[issues]: https://github.com/jorenham/optype/issues
+
+!!! note
+
+ `Generic` shadows `typing.Generic`, and `Array` means something different
+ here than it does in the parent `optype.numpy` namespace. Both are
+ intentional — the names mirror `np.generic` and `np.ndarray` — and are safe
+ as long as the module is imported as a namespace rather than star-imported.
+
+## Concrete types
+
+
+| alias | ctypes | NumPy analogue | kind |
+Bool | c_bool | np.bool_ | boolean |
+Int8 | c_int8 | np.int8 | fixed-width integer |
+UInt8 | c_uint8 | np.uint8 | fixed-width integer |
+Int16 | c_int16 | np.int16 | fixed-width integer |
+UInt16 | c_uint16 | np.uint16 | fixed-width integer |
+Int32 | c_int32 | np.int32 | fixed-width integer |
+UInt32 | c_uint32 | np.uint32 | fixed-width integer |
+Int64 | c_int64 | np.int64 | fixed-width integer |
+UInt64 | c_uint64 | np.uint64 | fixed-width integer |
+Byte | c_byte | np.byte | C-native integer |
+UByte | c_ubyte | np.ubyte | C-native integer |
+Short | c_short | np.short | C-native integer |
+UShort | c_ushort | np.ushort | C-native integer |
+IntC | c_int | np.intc | C-native integer |
+UIntC | c_uint | np.uintc | C-native integer |
+IntP | c_ssize_t | np.intp | C-native integer |
+UIntP | c_size_t | np.uintp | C-native integer |
+Long | c_long | np.long | C-native integer |
+ULong | c_ulong | np.ulong | C-native integer |
+LongLong | c_longlong | np.longlong | C-native integer |
+ULongLong | c_ulonglong | np.ulonglong | C-native integer |
+Float32 | c_float | np.float32 | floating-point |
+Float64 | c_double | np.float64 | floating-point |
+LongDouble | c_longdouble | np.longdouble | floating-point |
+Complex64 | c_float_complex | np.complex64 | complex floating-point |
+Complex128 | c_double_complex | np.complex128 | complex floating-point |
+CLongDouble | c_longdouble_complex | np.clongdouble | complex floating-point |
+Bytes | c_char | np.bytes_ | character/byte |
+Object | py_object | np.object_ | Python object reference |
+
+
+### Aliased names
+
+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:
+
+
+| alias | ILP32 | LP64 | LLP64 |
+Int8 | Byte | Byte | Byte |
+UInt8 | UByte | UByte | UByte |
+Int16 | Short | Short | Short |
+UInt16 | UShort | UShort | UShort |
+Int32 | IntC = Long | IntC | IntC = Long |
+UInt32 | UIntC = ULong | UIntC | UIntC = ULong |
+Int64 | LongLong | Long = LongLong | LongLong |
+UInt64 | ULongLong | ULong = ULongLong | ULongLong |
+IntP | IntC = Long | Long = LongLong | LongLong |
+UIntP | UIntC = ULong | ULong = ULongLong | ULongLong |
+
+
+`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.
+
+### `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.
+
+### Complex types
+
+`c_float_complex`, `c_double_complex`, and `c_longdouble_complex` were added in
+Python 3.14 and are not available on Windows.
+
+Where they are unavailable — on Windows, or on any Python before 3.14 —
+`Complex64`, `Complex128`, and `CLongDouble` are still importable but are
+aliases of `Never`. They are therefore uninhabited: any value assigned to them
+is a type error, and unions containing them silently drop those members.
+
+### NumPy version notes
+
+`UIntP` is `c_size_t`. On `numpy < 2`, `np.uintp` was `c_void_p` rather than
+`c_size_t`. The two have the same width on every supported data model and are
+almost always equivalent, but they remain distinct classes with different
+`.value` semantics: `c_void_p(0).value` is `None`, where `c_size_t(0).value`
+is `0`.
+
+`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
+
+`np.float16`, `np.str_`, `np.datetime64`, and `np.timedelta64` have no `ctypes`
+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
+[`np.ctypeslib.as_ctypes_type`][as_ctypes_type]. Note that `c_char` is a single
+byte whereas `np.bytes_` is variable-length; the correspondence is with the
+dtype's element type, not its length.
+
+## Abstract type aliases
+
+`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
+annotations.
+
+
+| alias | definition |
+CType | ct._CData |
+CScalar[T] | ct._SimpleCData[T] |
+Array[CT: CType] | ct.Array[CT] | ct.Array[Array[CT]] |
+
+
+`Array` is recursive, so it matches arbitrarily nested `ctypes` arrays —
+`c_int * 3`, `c_int * 3 * 4`, and deeper.
+
+The remaining aliases mirror the `np.generic` hierarchy:
+
+
+| alias | C types |
+SignedInteger | Int8 | Int16 | Int32 | Int64 | Short | IntC | IntP | Long | LongLong |
+UnsignedInteger | UInt8 | UInt16 | UInt32 | UInt64 | UShort | UIntC | UIntP | ULong | ULongLong |
+Integer | SignedInteger | UnsignedInteger |
+Floating | Float32 | Float64 | LongDouble |
+ComplexFloating | Complex64 | Complex128 | CLongDouble |
+Inexact | Floating | ComplexFloating |
+Number | Integer | Inexact |
+Void | ct.Structure | ct.Union |
+Flexible | Bytes | Void |
+Generic | Bool | Number | Flexible | Object |
+
+
+`Byte` and `UByte` are absent from the two integer unions because they are
+aliases of `Int8` and `UInt8`; including them would be redundant.
+
+### 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`.
+
+## 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)()
+```
diff --git a/zensical.toml b/zensical.toml
index a3dd3a0c..4d3cb52b 100644
--- a/zensical.toml
+++ b/zensical.toml
@@ -27,7 +27,7 @@ extra_javascript = ["scripts/posthog.js"]
"reference/index.md",
{ "Core Types" = ["reference/core/just.md", "reference/core/conversion.md", "reference/core/relations.md", "reference/core/binary.md", "reference/core/reflected.md", "reference/core/inplace.md", "reference/core/unary.md", "reference/core/rounding.md", "reference/core/callables.md", "reference/core/iteration.md", "reference/core/awaitables.md", "reference/core/async-iteration.md", "reference/core/containers.md", "reference/core/attributes.md", "reference/core/context.md", "reference/core/descriptors.md", "reference/core/metaclasses.md", "reference/core/buffer.md"] },
{ "Standard Library" = ["reference/stdlib/copy.md", "reference/stdlib/dataclasses.md", "reference/stdlib/inspect.md", "reference/stdlib/io.md", "reference/stdlib/json.md", "reference/stdlib/pickle.md", "reference/stdlib/string.md", "reference/stdlib/typing.md", "reference/stdlib/dlpack.md"] },
- { "NumPy" = ["reference/numpy/index.md", "reference/numpy/shape.md", "reference/numpy/array-likes.md", "reference/numpy/literals.md", "reference/numpy/compat.md", "reference/numpy/random.md", "reference/numpy/dtype.md", "reference/numpy/scalar.md", "reference/numpy/ufunc.md", "reference/numpy/aliases.md", "reference/numpy/low-level.md"] },
+ { "NumPy" = ["reference/numpy/index.md", "reference/numpy/shape.md", "reference/numpy/array-likes.md", "reference/numpy/literals.md", "reference/numpy/compat.md", "reference/numpy/random.md", "reference/numpy/dtype.md", "reference/numpy/scalar.md", "reference/numpy/ufunc.md", "reference/numpy/aliases.md", "reference/numpy/low-level.md", "reference/numpy/ctypeslib.md"] },
{ "Experimental" = ["reference/experimental/infer.md", "reference/experimental/test.md"] },
]