pyrefly-warp-stubs
NVIDIA Warp type stubs with array and tile
shape tracking for Pyrefly,
in the spirit of Pyrefly's pyrefly-numpy-stubs and pyrefly-torch-stubs.
@wp.kernel
def mlp(x: wp.array2d[float], w: wp.array2d[float], out: wp.array2d[float]):
inputs = wp.tile_load(x, shape=(16, 32)) # Tile[float, [16, 32]]
weights = wp.tile_load(w, shape=(64, 16)) # Tile[float, [64, 16]]
hidden = wp.tile_matmul(weights, inputs) # Tile[float, [64, 32]]
wp.tile_matmul(inputs, weights) # error: expected Int[32], got Int[64]
bias = wp.tile_load(w, shape=(64, 1))
hidden + bias # error: tiles never broadcast implicitly
wp.tile_sum(hidden, axis=2) # error: reduction axis out of bounds
Tile shapes in Warp are compile-time constants, so a shape bug normally only
surfaces when the kernel is built at launch. With these stubs Pyrefly reports it
in the editor, along with Warp's scalar and vector type rules
(wp.max(f64, 0.0), vec3 * vec3, mat44 * vec3 are errors, as in Warp).
Install
Add it as a development dependency of a project that uses Warp:
uv add --dev pyrefly-warp-stubs # or: pip install pyrefly-warp-stubs
# latest main: uv add --dev "pyrefly-warp-stubs @ git+https://github.com/julien-blanchon/pyrefly-warp-stubs"
This installs warp-stubs (a PEP 561 partial stub package) and
pyrefly-shape-extensions. Pyrefly picks the stubs up automatically. Requires
Python ≥ 3.12 and Pyrefly ≥ 1.4, in default or strict mode: add a
pyrefly.toml (or [tool.pyrefly] in pyproject.toml), since the basic
mode used without one hides shape errors.
The stubs are generated from warp-lang 1.18. Newer Warp releases install
fine, but the stubs replace Warp's own __init__.pyi, so functions added after
1.18 are reported as missing until the stubs are regenerated (see
Development).
Using it in your project
The stubs only exist for the type checker: Python never imports .pyi files,
so nothing changes at runtime, and people who install your package never
download these stubs or need Pyrefly. That holds as long as shape annotations
stay type-checking-only:
from __future__ import annotations # required: see below
from typing import TYPE_CHECKING
import warp as wp
if TYPE_CHECKING: # never imported at runtime
from shape_extensions import IntVar
def normalize[N: IntVar](points: wp.array[wp.vec3, [N]]) -> wp.array[wp.vec3, [N]]:
...
normalize(wp.zeros(10, dtype=wp.vec3)) # ok: array[vec3f, [10]]
normalize(wp.zeros((10, 3), dtype=float)) # error: array[float, [10, 3]]
- Keep
from __future__ import annotationsin every file with shaped annotations. Without it Python evaluateswp.array[wp.vec3, [N]]at import time, and Warp rejects anything but an integerndimthere. - Import
shape_extensionsonly underTYPE_CHECKING. Usingassert_shape,shape_varsor a top-levelfrom shape_extensions import ...in shipped code makespyrefly-shape-extensionsa runtime dependency. - No shapes in
@wp.kernel/@wp.funcsignatures or@wp.structfields. Warp evaluates those annotations itself, even with the future import; keepwp.array2d[float]there. Shapes are still inferred inside the kernel body. - Shape assertions inside kernels go under
if TYPE_CHECKING:. Warp constant-folds the condition and never compiles the block, while Pyrefly checks it. A barereveal_type/assert_typein a kernel breaks the build.expect_shape(from the stub-onlywarp.shape_checks) checks against your tile-size constants instead of literals:t = wp.tile_load(a, shape=(TILE_M, TILE_N)) if TYPE_CHECKING: expect_shape(t, (TILE_M, TILE_N)) assert_type(t, Tile[float, [8, 4]])
- Quote shaped types in executed
assert_type/cast:assert_type(x, "wp.array[float, [10, 3]]"). Their type argument is evaluated when the line runs, even with the future import. Shaped type aliases must use PEP 695 (type Points = wp.array[wp.vec3, [int]]). Tile,Array,Vector,warp.shape_checks, ... exist only in the stubs: import and use them underif TYPE_CHECKING:or in annotations.- Public API. Other type checkers (Pyright, mypy, ty) cannot read
wp.array[wp.vec3, [N]], so a downstream user without these stubs sees an untyped parameter. If that matters, use the portableShaped[wp.array[wp.vec3], "[N]"]inside a@shape_vars("N")function, which other checkers read as plainwp.array[wp.vec3]. That decorator runs at import time, so it does make the small, pure-Pythonpyrefly-shape-extensionsa runtime dependency of your package. - Contributors using other checkers. Pyright or ty in an environment with
your dev dependencies will also pick up
warp-stubsand report its shape syntax as errors. This only affects people developing your project.
skills/add-shape-types-to-warp-project/ contains an agent skill that applies
all of this step by step, and verify_port.py, a lint for these runtime rules
(Pyrefly cannot see them).
Shapes on kernel inputs: typed launchers
Kernel signatures must stay plain Warp, and wp.launch(inputs=[...]) is not
checked against them. Put the shapes on the host function that launches the
kernel; Pyrefly then checks every call, and the dimensions flow to the result:
def matmul[M: IntVar, K: IntVar, N: IntVar](
a: wp.array[float, [M, K]], b: wp.array[float, [K, N]]
) -> wp.array[float, [M, N]]:
c = wp.zeros((a.shape[0], b.shape[1]), dtype=float) # array[float, [M, N]]
wp.launch_tiled(gemm_kernel, dim=..., inputs=[a, b, c], block_dim=64)
return c
matmul(wp.empty((64, 32)), wp.empty((32, 48))) # array[float, [64, 48]]
matmul(wp.empty((32, 48)), wp.empty((64, 32))) # error: expected [48, @_]
Inside the kernel, a, b and c are only array[float, [int, int]]: a
kernel cannot state that two parameters share a shape (a generic kernel
signature fails when Warp evaluates it), and inputs=[...] is not checked
against the kernel. Allocating outputs from the inputs in the launcher
(wp.zeros((a.shape[0], b.shape[1])), wp.empty_like(x)) enforces the
relationship by construction and leaves that one line as the only unchecked
link.
Named dimensions (B C H W)
Named dimensions are type parameters. Three spellings check the same way:
| Spelling | Needs at runtime |
|---|---|
def f[B: IntVar, C: IntVar, H: IntVar, W: IntVar](x: wp.array[float, [B, C, H, W]]) |
nothing (IntVar imported under TYPE_CHECKING) |
@shape_vars("B, C, H, W") and Shaped[wp.array[float], "[B, C, H, W]"] |
pyrefly-shape-extensions |
@static_jaxtyping("B C H W") and Float[wp.array, "B C H W"] |
jaxtyping and pyrefly-shape-extensions (no dtype) |
Arithmetic on them is tracked: x.reshape((x.shape[0], x.shape[1], -1)) on a
[B, C, H, W] array is [B, C, H * W] (see examples/image_batch.py).
Libraries: a typing extra
If your library uses Warp and wants its users to get the shape types too, offer the stubs as an extra, not only a dev dependency: dependency groups (PEP 735) are never published, extras are.
[project.optional-dependencies]
typing = ["pyrefly-warp-stubs>=0.1"] # users opt in: pip install mylib[typing]
[dependency-groups]
dev = ["mylib[typing]", "pyrefly>=1.4", "pytest"] # contributors get it by default
A plain pip install mylib still installs only Warp. Keep the published
requirement a plain name (pyrefly-warp-stubs>=0.1): build backends refuse git
URLs in optional-dependencies and PyPI rejects them. To develop against the
latest main instead, add a source that is not published:
[tool.uv.sources]
pyrefly-warp-stubs = { git = "https://github.com/julien-blanchon/pyrefly-warp-stubs" }
What is tracked
| Area | Examples |
|---|---|
| Array creation | wp.zeros((128, 3), dtype=wp.vec3) → array[vec3f, [128, 3]]; zeros/ones/empty/full (dtype from value), wp.array(shape=...), list literals, from_numpy, *_like, clone |
| Array views | reshape (with -1), flatten, transpose, slicing a[1:5, ::2], contiguous, to, grad |
| Rank aliases | wp.array2d[float] is array[float, [int, int]]; passing a 2D array where array3d is expected is an error |
| Kernel indexing | i, j = wp.tid(); a[i, j] → element type; a[i] → row view; too many indices is an error |
| Scalars | float is wp.float32, int is wp.int32, bool is wp.bool, as in Warp. No implicit conversions: wp.max(f32, 0.0) is fine, wp.max(f64, 0.0) and f32 + f64 are errors |
| Vectors, matrices | wp.vec3, wp.vec3f and Vector[float, Literal[3]] are one type. v + v, v * s, m * v ([R, C] × [C] → [R]), m @ m, q * q, t.p, v.x are typed; v * v, v + 1.0, v * f64, mat44 * vec3, q * v are errors, as in Warp |
| Tile creation | tile_zeros/ones/empty/full/randf/randi, tile_arange(start, stop, step) |
| Global memory | tile_load/tile_store/tile_atomic_add (tile rank must match array rank), *_indexed |
| Tile layout | tile_transpose, tile_reshape, tile_squeeze, tile_broadcast (NumPy broadcast_to rules), tile_view, tile_astype, subscripts t[i], t[1:3, :], t[i, j] |
| Tile operators | + - * / @ between identical shapes, * / with scalars, unary -, & | ^ on integer tiles. t + 1.0, +t, t % u are errors, as in Warp |
| Tile math | tile_map, tile_matmul (incl. out=), tile_dot, tile_sum/min/max/argmin/argmax/reduce (with axis), scans |
| Linear algebra | tile_matmul, tile_cholesky, *_solve, *_solve_inplace (float tiles, square / matching extents), tile_diag_add |
warp.fem |
Fields, domains, operators and integrands are callable inside integrands (u(s), fem.inner(u, s)); @wp.func overloads such as fem.make_free_sample resolve per argument type |
| Textures, meshes, volumes | wp.texture_sample(tex, uvw, dtype=wp.vec4) is a vec4 (likewise volume_sample, volume_lookup, cast); wp.mesh_query_point(...) / mesh_query_ray results have typed result, face, u, v, t, normal; wp.mesh_get(id).points[i] is a vec3; for face in wp.mesh_query_aabb(...) and hash-grid queries iterate (BVH queries do not, as in Warp); mesh.id, grid.id, ... are uint64 |
Literal sizes, module-level constants (TILE = 64) and wp.constant(64) are
tracked; sizes only known at runtime become int dimensions, which keep the rank.
Every operator and rule above was checked against what Warp 1.18 actually builds, by compiling kernels on the CPU; the tests keep it that way.
Examples
Each example runs on the CPU (python examples/<name>.py), checks its result
against NumPy, and type-checks cleanly; its docstring lists errors Pyrefly
reports for typical mistakes (all verified by the test suite or by hand).
| Example | Shows |
|---|---|
tile_mlp.py |
tile matmul/broadcast/map, expect_shape in a kernel, a typed launcher generic in the batch size |
blocked_matmul.py |
tile offsets, tile_matmul(a, b, out) accumulation, [M, K] @ [K, N] -> [M, N] |
row_softmax.py |
axis reductions, tile_reshape, explicit tile_broadcast, element-wise tile math |
image_batch.py |
named dimensions B, C, H, W, symbolic H * W, views, a 4-D launch |
particles.py |
vec3/mat33 arithmetic, scalar rules, class-level array declarations, a shared N |
How it works
Pyrefly treats any class with a type parameter bound to IntTuple as a shaped
array. Warp already declares Array(Generic[DType, NDim]) and
Tile(Generic[DType, Shape]). These stubs re-declare them as
Array[DType, Shape: IntTuple] and Tile[DType, Shape: IntTuple], keeping the
shape second so that wp.array[float] still means "dtype float".
Warp's __init__.pyi is ~11k autogenerated lines, and the classes live in
warp/_src, so the stubs are generated rather than hand-written:
overrides/<path>.pyi ─┐ ┌─> warp-stubs/<path>.pyi
installed warp package ─┴─ tools/generate_stubs.py ┘ (_shapes.pyi, shape_checks.pyi are hand-written)
Each file under overrides/ stubs the Warp module at the same path (12 modules:
__init__, _src/types, renderers, sparse, several fem modules).
tools/generate_stubs.py copies the module and merges in the overrides:
- An override
defreplaces every upstream overload of that name; the upstream docstring is carried over (also for functions upstream only re-exports). - An override
classmerges into the upstream class: it supplies the header and the members it redefines, and keeps the rest. rewrite_paramswidens upstream parameter annotations that are narrower than what the code accepts (e.g. rendererpos: tuplealso takes awp.vec3).- Warp's
@wp.funcoverloading by redefinition becomestyping.overloadgroups; otherwise the last definition would shadow the others. - An override import may not silently rebind a name upstream imports from
elsewhere (an early version replaced
wp.overloadwithtyping.overload). - Function bodies are kept: Pyrefly infers attributes and return types from them even in stubs.
Shape computations that need logic (reshape, axis reductions, broadcasting,
tile_arange) are Pyrefly shape DSL
rules in warp-stubs/_shapes.pyi.
Fixes to upstream typing
Precise element types expose gaps that were hidden while a[i] was untyped, so
the overrides also fix these:
- Scalars:
float/int/boolandwp.float32/wp.int32/wp.boolwere distinct types, sowp.max(a[i], 0.0)and passing adtype=floatarray to awp.float32parameter were errors. Scalar operators are typed. - Vectors: upstream has two unrelated
vec3fclasses (warp/__init__.pyiandwarp._src.types), neither aVector, with constructors that reject valid calls (wp.vec3(np_array)). They are now one type with Warp's operators;wp.types.vector(...)andwp.spatial_vectorare vectors too. wp.tid()isint | tuple[int, int] | ...upstream, which cannot be unpacked asi, j = wp.tid(). It is now anintthat can also be unpacked.warp.fem: fields, domains, operators and integrands were not callable (360 errors on Warp's FEM examples), and@integrandwas typed as a union.- Kernel-only fields that Warp declares solely in its code-generation registry
(
MeshQueryPoint.face,Texture3D.depth, ...) are read by the generator and declared;dtype-driven builtins (texture_sample,volume_sample,cast) return the requested type instead ofAny. for i in wp.hash_grid_query(...)is iterable;wp.where/wp.selectkeep the value type;tile_*_solve_inplaceaccept 2D right-hand sides;tile_fftand the*_indexedfunctions accept any element type;wp.indexedarray1d[...]–4d[...]work in annotations;fem.Grid2D/Grid3Dand the renderers accept the vector values Warp's examples pass them.
On Warp's own 92 example scripts, Pyrefly reports 370 errors with these stubs
versus 2145 without. Nine errors are new: one real bug in the examples
(range(num_bodies / TILE_SIZE) passes a float), four where
example_diffusion_3d.py passes 3D bounds to a 2D helper annotated wp.vec2,
and four from a Pyrefly bug (below). See tools/compare_examples.py.
Known limitations
wp.array[dtype, Literal[ndim]]cannot be expressed: the second parameter is now the shape. Usewp.array2d[dtype]etc.- Tile shapes cannot be written in kernel signatures or kernel-local annotations: Warp evaluates kernel annotations at runtime and does not support annotated assignments in kernels. Shapes are inferred inside kernel bodies.
wp.array[float](no rank) is an array of unknown rank; usewp.array1d[...]to get rank checks.wp.launch(kernel, inputs=[...])arguments are not checked against the kernel signature.wp.tile[float, M, N]annotations on@wp.funcparameters are valid Warp, but Pyrefly 1.4 cannot express a name that is both thewp.tile(x)builtin and a variadic shape type, so those parameters areUnknown(as upstream).- Module constants cannot be used as dimensions in annotations (
M = 4,Tile[float, [M, 8]]); no spelling works in Pyrefly 1.4. Use literals,IntVarparameters, orexpect_shape(t, (M, 8)). Constants are tracked in calls (shape=(M, 8)). - Typing lets
intpass wherefloatis expected, sowp.int32(which isint) tiles reach float-only tile operations, andwp.max(f32, 1)passes; Warp rejects both. Other integer types (wp.int64,wp.uint32, ...) are caught. - A list literal passed inline as an argument (
f(wp.array([0.5, 0.5]))) is typed from the expected parameter and not checked against it; assigned to a variable first, it is. .won a 3-vector is not caught; the length fromwp.types.vector(length=5, ...)is widened toint.@wp.funcoverloading by redefinition in your code: the checker sees the last definition only (the generator fixes this for Warp's own modules).- Results of
tile_map, of field evaluations inwarp.fem, and of builtins that upstream declares asVector[Scalar, Any](wp.normalize) are not length- or dtype-tracked. wp.spatial_vectoris a function in upstream's stubs (the kernel constructor), so it cannot be used as a type; usewp.spatial_vectorf.- Pyrefly 1.4 issues found while building this:
- A shape list spelled with
builtins.int([builtins.int, builtins.int]) silently becomesUnknown. Bareintworks. - Calling an unspecialised generic alias (
self.x = wp.array2d(data)) reports the alias's type variable as leaking (invalid-type-var), even with adefault=Any.
- A shape list spelled with
Development
uv venv -p 3.13 && uv pip install pyrefly==1.4.0 pyrefly-shape-extensions==1.4.0 \
"warp-lang==1.18.*" numpy pytest
.venv/bin/python tools/generate_stubs.py # after editing overrides/
.venv/bin/python -m pytest tests/ # static + runtime suites
.venv/bin/python tools/compare_examples.py # errors added/removed on Warp's examples
Each test file pairs static check_* functions (assert_type, with an
# E: <message> comment on lines that must error, checked by
pyrefly check --expectations) with runtime test_* functions. The runtime
functions launch the same operations on Warp's CPU backend: well-typed kernels
must match NumPy, and every kernel Pyrefly rejects must also fail to build in
Warp. tests/test_static.py runs the Pyrefly half, checks that the generated
stubs are up to date, and validates the DSL rules; tests/test_verify_port.py
pins the lint.
To target another Warp release: install it, regenerate, and run the tests.
Overrides of functions whose upstream signature changed will show up as test
failures or new errors in compare_examples.py.
License
The generated stubs contain portions of Warp's source, stubs and docstrings
(Apache-2.0, © NVIDIA). warp-stubs/_shapes.pyi adapts rules from
pyrefly-numpy-stubs (MIT, © Meta Platforms).
Metadata
Release files for pyrefly-warp-stubs 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pyrefly_warp_stubs-0.1.1.tar.gz | 218.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyrefly_warp_stubs-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 448.8 kB
Release files / pyrefly_warp_stubs-0.1.1.tar.gz
| Download URL | pyrefly_warp_stubs-0.1.1.tar.gz |
|---|---|
| Size | 218.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64c0abbf13f4521fa214855493ec3753be3407f7ac8d62859dc9ec0086eed9fc
|
|
BLAKE2b-256 checksum How to use checksums |
b98a150b6b84bc5b88ffccb6171548deff6d57690a68713935d2235b25eebfb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pyrefly_warp_stubs-0.1.1-py3-none-any.whl
| Download URL | pyrefly_warp_stubs-0.1.1-py3-none-any.whl |
|---|---|
| Size | 230.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
24bff6fa2c9930199655ec0c9d4ae4714261c9dc424fc23ab05d2fb2930187b4
|
|
BLAKE2b-256 checksum How to use checksums |
527f471802611eafbca0223fe65774ef54caaca8d655fc272dbcdd25edd4b99f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for pyrefly-warp-stubs 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.