mtlpy
Python bindings for GPU compute on Apple Metal, built on pybind11 and Apple's metal-cpp. Write a Metal compute kernel as a string, dispatch it over NumPy arrays, get the result back as a NumPy array — no separate build step, no manual buffer plumbing.
import numpy as np
import mtlpy
device = mtlpy.Device()
a = device.buffer(np.array([1.0, 2.0, 3.0], dtype=np.float32))
b = device.buffer(np.array([10.0, 20.0, 30.0], dtype=np.float32))
print((a + b).contents) # [11. 22. 33.]
Why this exists
mtlpy is a from-scratch rewrite of themetalgpu
project. Rather than build on top of that codebase's ctypes-based bindings
and global singleton state, mtlpy starts over with a few deliberate
improvements:
- pybind11 instead of ctypes — real type safety across the Python/C++ boundary, and Metal errors propagate as Python exceptions instead of silent failures.
- No global singleton state — each
Deviceowns its own command queue and pipeline cache; nothing is saved/restored behind your back. - Pipeline compile caching — a compute pipeline is compiled once per (shader source, function name) and reused, both within a process and (via an on-disk Metal binary archive) across process launches.
- Async dispatch —
Pipeline.run(..., wait=False)lets you batch work without stalling on every call.
Status
Alpha, but built, tested, and benchmarked on real Apple Silicon hardware — see Building from source and the test suite for current coverage.
Architecture
metal-cpp/ Apple's C++ Metal headers (git submodule)
csrc/ C++ extension (pybind11 + metal-cpp)
device.{h,cpp} MTL::Device + MTL::CommandQueue owner
buffer.{h,cpp} MTL::Buffer wrapper (shared-storage, CPU/GPU unified memory)
texture.{h,cpp} MTL::Texture wrapper (1D/2D/3D)
sampler.{h,cpp} MTL::SamplerState wrapper
pipeline.{h,cpp} Dispatches a compiled MTL::ComputePipelineState
command_buffer.{h,cpp} Batches multiple Pipeline::run() dispatches into
one MTL::CommandBuffer submission
pipeline_cache.{h,cpp} Compiles-once cache, keyed on (source, function name),
backed by an on-disk MTL::BinaryArchive
metal_impl.mm Single Obj-C++ translation unit providing the
NS::/CA::/MTL:: private implementations
bindings.cpp pybind11 module definition (`_mtlpy`)
src/mtlpy/ Python package (src layout, for PyPI)
device.py Device: buffer/empty/texture/sampler/compile, list_devices(), wraps _mtlpy.Device
buffer.py Buffer: NumPy-backed contents, arithmetic/comparison/in-place operators
texture.py Texture, Sampler: wrap _mtlpy.Texture/_mtlpy.Sampler
pipeline.py Pipeline, CommandBuffer: thin wrappers over _mtlpy.Pipeline/CommandBuffer
operators.py sqrt/cos/sin/tan/exp/log, sum/max/min/mean reductions
shader.py Generates Metal Shading Language source per dtype/texture type
utils.py NumPy dtype <-> Metal type/pixel format mapping
tests/ pytest suite
benchmarks/ Standalone performance baseline scripts
examples/ Runnable usage examples
Each Device in Python owns exactly one MTL::Device, one MTL::CommandQueue,
and one PipelineCache. Buffers use MTL::ResourceStorageModeShared, so on
Apple Silicon's unified memory there's no copy between CPU and GPU views of
the same allocation — Buffer.contents is a NumPy array backed directly by
GPU-visible memory (accessing .contents is a true zero-copy view; writing
new data into it via buf.contents[:] = arr is still a real memcpy from
arr's own memory, same as it would be for any destination).
Features
- Elementwise operators:
+,-,*,/, unary-, and in-place+=/-=/*=//=(which dispatch in-place, into the sameBuffer, with no extra allocation) onBuffer— each also works with a NumPy/Python scalar on either side (buf + 5.0,5.0 - buf), not justBuffer op Buffer. Plussqrt,cos,sin,tan,exp,log, andastypefor dtype conversion. - Comparisons:
==,!=,<,<=,>,>=(against anotherBufferor a scalar) return aboolBuffer, matching NumPy'sndarrayconvention — which also makesBufferunhashable, same tradeoff NumPy makes. - Reductions:
operators.sum/max/min/mean— an O(log n) multi-pass tree reduction returning a plain Python scalar. - Custom kernels: compile and dispatch arbitrary Metal Shading Language
source directly (see Custom kernels below).
Pipeline.runvalidates the buffer/texture/sampler counts against the kernel's own argument reflection, so passing too few of any of them raises a clear Python exception instead of leaving a Metal argument unbound (undefined behavior). - Textures: 1D/2D/3D
Textures (device.texture(array, pixel_format)/device.empty_texture(shape, pixel_format)) andSamplers for kernels written againsttexture2d<...>/etc. rather than raw buffers -- see Textures below. - Shapes:
Buffer.shapetracks the logical shape a buffer was created orreshape()d with (elementwise ops preserve it);Buffer.numpy()/np.asarray(buf)return contents in that shape.Buffer.contentsitself stays flat regardless — see Shapes and NumPy interop. - Dtype support:
float32,float16,int32,uint32,int16,uint16,int64,uint64,bool— mapped to their Metal equivalents (float,half,int,uint,short,ushort,long,ulong,bool) insrc/mtlpy/utils.py.float64has no Metal equivalent (no Apple GPU supports double precision), so it's silently downcast tofloat32at buffer creation. Note thatBuffer / Bufferuses Metal's native/for the shared dtype (truncating for integers), not NumPy's always-promote-to-float64 semantics. - Pipeline caching: identical (source, function name) pairs are compiled
once per process and reused; a binary archive on disk
(
~/Library/Caches/mtlpy/pipelines.metallib) carries compiled pipelines across process launches too.Device.flush_cache()(or usingDeviceas a context manager:with mtlpy.Device() as d:) serializes it on demand, rather than only when theDeviceis garbage collected. - Async dispatch:
wait=Falsecommits work without blocking; Metal retires command buffers on a queue in commit order, so a laterwait=Truedispatch that reads the result is enough to synchronize (seeexamples/async_dispatch.py).Pipeline.runreleases the GIL for the whole call, so other Python threads keep running during the GPU wait instead of being blocked for its full duration. - Batched dispatches:
Device.command_buffer()batches multiplePipeline.run()calls into oneMTLCommandBuffersubmission instead of one per dispatch -- see Batching dispatches below. - Multi-GPU support:
mtlpy.list_devices()lists every Metal-capable GPU on the machine;mtlpy.Device(index=...)selects one (the default targets the system default GPU). - Errors as exceptions: shader compile failures, missing kernel
functions, mismatched buffer counts, mismatched-
Deviceoperands, and GPU execution errors all raise Python exceptions with a clear message, instead of failing silently or invoking undefined behavior.
Building from source
Requires macOS with Metal support, Xcode (for the Metal/Objective-C++ toolchain), CMake, and Python 3.9+.
git clone --recursive git@github.com:peyton-howe/mtlpy.git
cd mtlpy
pip install -e ".[dev]"
If you already cloned without --recursive:
git submodule update --init
Quick start
import numpy as np
import mtlpy
device = mtlpy.Device()
a = device.buffer(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32))
b = device.buffer(np.array([10.0, 20.0, 30.0, 40.0], dtype=np.float32))
c = a + b
print(c.contents) # numpy.ndarray([11. 22. 33. 44.])
d = mtlpy.operators.sqrt(a)
print(d.contents)
e = a.astype(np.int32)
print(e.dtype, e.contents)
Custom kernels
Device.compile(source, function_name) compiles arbitrary Metal Shading
Language and returns a Pipeline you can dispatch directly:
source = """
#include <metal_stdlib>
using namespace metal;
kernel void square(
device const float *a [[buffer(0)]],
device float *b [[buffer(1)]],
uint id [[thread_position_in_grid]])
{
b[id] = a[id] * a[id];
}
"""
pipeline = device.compile(source, "square")
a = device.buffer(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32))
b = device.empty(4, np.float32)
pipeline.run([a, b], grid=4)
print(b.contents) # [1. 4. 9. 16.]
grid may be an int (1D dispatch) or a 3-tuple/list for 2D/3D dispatch.
Threadgroup sizing is computed automatically from the pipeline's
thread_execution_width and max_threads_per_threadgroup.
Shapes and NumPy interop
Buffer.contents is deliberately always flat (see
Custom kernels and the note above on unified memory) — but
every Buffer also tracks a logical .shape, set when you create it from an
ndarray or via device.empty(shape, dtype), and preserved by the elementwise
operators:
img = np.arange(24, dtype=np.float32).reshape(4, 6)
buf = device.buffer(img)
buf.shape # (4, 6)
buf.contents.shape # (24,) -- always flat
buf.numpy().shape # (4, 6) -- contents reshaped to buf.shape, still zero-copy
np.asarray(buf) # same as buf.numpy() -- Buffer implements __array__
np.array(buf, dtype=np.float64) # dtype conversion via the same protocol
grid = buf.reshape(2, 12) # new Buffer, same underlying Metal allocation
.numpy() and __array__ are both zero-copy views, same as .contents —
reshaping a flat contiguous array is always a view in NumPy, never a copy, so
none of this allocates or duplicates GPU memory. .reshape() similarly
shares the same MTL::Buffer rather than reallocating.
Elementwise operators (+, -, *, comparisons, ...) check .size, not
.shape — a Metal buffer has no shape of its own (it's just bytes), so two
buffers with equal flat size but different declared .shape are still valid
operands; the result takes the first operand's .shape. .shape is
purely Python-side bookkeeping layered on top, not something Metal itself
knows about.
Textures
Texture wraps MTL::Texture (1D/2D/3D) for kernels that want
texture2d<...>-style access instead of raw device float* buffers --
useful for image-processing-style kernels, and for sampling (bilinear
filtering, addressing modes) rather than plain indexing.
img = np.random.rand(64, 64).astype(np.float32) # (height, width)
tex = device.texture(img, "r32Float") # uploads in one call
invert_source = """
#include <metal_stdlib>
using namespace metal;
kernel void invert(
texture2d<float, access::read_write> tex [[texture(0)]],
uint2 gid [[thread_position_in_grid]])
{
float4 c = tex.read(gid);
tex.write(1.0 - c, gid);
}
"""
pipeline = device.compile(invert_source, "invert")
pipeline.run([], grid=(64, 64, 1), textures=[tex]) # no buffers, one texture
result = tex.download() # or np.asarray(tex) / tex.numpy()
Pipeline.run takes buffers, textures, and samplers as separate lists
because Metal Shading Language gives each its own independent binding
namespace ([[buffer(n)]] / [[texture(n)]] / [[sampler(n)]]) -- list
position i binds to index i in that namespace, same convention buffers
already use. A sampling kernel needs a Sampler too:
sample_source = """
#include <metal_stdlib>
using namespace metal;
kernel void downscale(
texture2d<float, access::sample> src [[texture(0)]],
texture2d<float, access::write> dst [[texture(1)]],
sampler smp [[sampler(0)]],
uint2 gid [[thread_position_in_grid]])
{
float2 uv = (float2(gid) + 0.5) / float2(dst.get_width(), dst.get_height());
dst.write(src.sample(smp, uv), gid);
}
"""
small = device.empty_texture((32, 32), "r32Float")
smp = device.sampler(linear=True) # linear=False for nearest-neighbor
downscale = device.compile(sample_source, "downscale")
downscale.run([], grid=(32, 32, 1), textures=[tex, small], samplers=[smp])
A few things that differ from Buffer:
- No zero-copy
.contents. Metal doesn't guarantee a texture's CPU-visible memory is a tightly packed array the way a Shared-storageBuffer's is (rows can be padded/tiled internally), so there's noBuffer.contentsequivalent..upload()/.download()(and.numpy()/__array__, which call.download()) are genuine copies viaMTL::Texture'sreplaceRegion/getBytes. shapeexcludes the channel count on input, includes it on output.device.empty_texture(shape, pixel_format)takes a spatial shape only --(width,),(height, width), or(depth, height, width)-- with channel count implied bypixel_format("rgba8Unorm"is 4-channel,"r32Float"is 1-channel).Texture.shape(and what.download()returns) appends a trailing channel dim whenchannels > 1, matching common image-array conventions.empty_textureraises if a 3-elementshapelooks like it still has that channel axis attached (its last element equals the format's channel count) -- if you have an(H, W, C)array, either usedevice.texture(data, pixel_format)(which strips it for you) or passshape[:-1].- Pixel formats, not dtypes. A small, deliberately non-exhaustive set
covering 8-bit and float image data (Metal defines 100+ pixel formats,
most for graphics rather than compute):
r8Unorm,rgba8Unorm,r16Float,rgba16Float,r32Float,rgba32Float,r32Uint,rgba32Uint(src/mtlpy/utils.py).Unormformats store small integers but kernels read/write them asfloatin[0, 1]-- Metal normalizes automatically;shader.texture_type(dims, msl_scalar_type, access)generates the right MSL type string (texture2d<float, access::sample>, etc.) if you don't want to hand-write it.
Moving data in and out of a Texture
device.texture(data, pixel_format) and .upload()/.download() (above)
are the simple default path -- always available, but CPU-side copies
(replaceRegion/getBytes) that only work on the default Shared storage
mode. There's a faster, GPU-side path for every CPU/Buffer/Texture
direction, each suited to a different data source and each working
regardless of storage mode (including private=True, see below) -- see
benchmarks/README.md for the measurements behind these:
| Direction | Method | Mechanism | Notes |
|---|---|---|---|
| CPU -> Texture | tex.upload(data) |
CPU replaceRegion |
Simple default; Shared storage only |
| CPU -> Texture | tex.upload_fast(data) |
CPU->Buffer memcpy + GPU blit |
Up to ~9x faster at 4K; works on private=True |
Buffer -> Texture |
tex.upload_from_buffer(buf) |
GPU blit | Same mechanism upload_fast uses, without the implicit staging Buffer -- use this directly if you already have a Buffer (e.g. reusing one across a hot loop instead of allocating fresh each call) |
| Texture -> CPU | tex.download() / .numpy() / np.asarray(tex) |
CPU getBytes |
Simple default; Shared storage only |
| Texture -> CPU | tex.download_fast() |
GPU compute kernel + Buffer->CPU (zero-copy) |
~1.5-1.6x faster at 1080p/4K; works on private=True; raises NotImplementedError on Unorm formats |
Texture -> Buffer |
tex.to_buffer() |
GPU compute kernel | What download_fast calls before .numpy() -- use this directly if you want the Buffer, not a numpy array (e.g. feeding it straight into another kernel) |
| Texture -> Texture | src.copy_to(dst) |
GPU blit | Works on any pixel format (including Unorm) and any Shared/Private combination -- e.g. copying a Shared texture you populated with .upload() into a Private one before a hot compute loop |
empty_texture(..., readable=, writable=, private=) controls usage flags
and storage mode at creation. private=True (MTLStorageModePrivate,
GPU-only memory) requires the GPU-side methods above -- .upload()/
.download() raise a clear error on a private texture, since Metal itself
rejects replaceRegion/getBytes on Private storage.
Reusing buffers in a hot loop
Buffer.contents is a live NumPy view over the same underlying Metal
allocation, not a copy — writing buf.contents[:] = ... updates GPU-visible
memory in place, and reading it back after a wait=True dispatch needs no
reallocation either. For a kernel dispatched repeatedly (e.g. in a while
loop), compile the pipeline and allocate buffers once, then just write/read
.contents each iteration:
pipeline = device.compile(source, "square")
a = device.buffer(np.zeros(4, dtype=np.float32)) # allocated once
out = device.empty(4, np.float32) # allocated once
while running:
a.contents[:] = get_next_input() # in-place write, no realloc
pipeline.run([a, out], grid=4) # wait=True by default
consume(out.contents) # in-place read, no realloc
The out-of-place convenience operators (a + b, operators.sqrt(a),
astype, etc.) don't follow this pattern — each call allocates a fresh
output Buffer internally, which is fine for one-off use but wasteful in a
tight loop. The in-place operators (a += b, a *= 2.0, ...) do reuse a's
own buffer with no extra allocation, if that fits your loop. See
examples/reuse_buffers.py.
Batching dispatches
Each Pipeline.run() call submits its own MTLCommandBuffer by default —
one command-buffer-create + commit + (if wait=True) waitUntilCompleted()
round trip per dispatch. For a fixed sequence of dispatches that always run
together (a multi-pass kernel, or any "run these N things then read the
result" pattern), Device.command_buffer() batches them into one submission
instead, as a context manager:
with device.command_buffer() as cb:
horizontal_pass.run([], grid, textures=[src, mid], cb=cb)
vertical_pass.run([], grid, textures=[mid, dst], cb=cb)
# one submit, one wait, covering both dispatches
Pipeline.run(..., cb=cb) encodes into cb's shared encoder instead of
committing its own command buffer — wait is ignored in that case (you
can't partially wait on part of a not-yet-committed command buffer), and it
always returns (0.0, 0.0): per-dispatch GPU timing isn't meaningful once
dispatches share a command buffer, only cb.commit()'s combined timing is.
The with block commits (and waits, by default) on normal exit; if the
block raises, it does not commit — a partially-encoded batch is discarded
rather than submitted, the same way a database transaction rolls back on
exception instead of committing a partial write. Measured ~2x faster than
two separate wait=True dispatches for a two-pass texture kernel.
Without a context manager: cb = device.command_buffer(), encode dispatches
into it the same way, then cb.commit(wait=True) (the default) or
cb.commit(wait=False) to defer waiting the same way Pipeline.run(..., wait=False) does — a later wait=True dispatch on the same queue still
guarantees this batch finished first (Metal retires command buffers on a
queue in commit order). Calling .commit() more than once, or encoding into
a CommandBuffer after it's committed, raises RuntimeError.
CommandBuffer vs. a plain wait=False chain (see Async
dispatch above): both avoid stalling between dependent
dispatches, but they're not interchangeable —
- Back-to-back dispatches with no CPU-side work between them: roughly tied either way.
- CPU-side work between dispatches (e.g. computing the next dispatch's
arguments): a
wait=Falsechain wins, measured ~1.2x faster for a 4K two-pass kernel with 2ms of CPU work in between.wait=Falsesubmits immediately, so the GPU starts executing that dispatch while the CPU is still busy;CommandBufferbatching defers all submission untilcommit(), so the GPU sits idle until the whole batch has been encoded. - Sequence not known upfront (the next dispatch depends on inspecting
something first): only a
wait=Falsechain fits -- nothing in aCommandBufferbatch executes until it's fully encoded and committed, so you can't make encoding decisions based on an earlier batched dispatch's result without breaking the batch anyway.
CommandBuffer is the better fit for a fixed, known-upfront sequence with
little CPU work in between (its original motivating case: a multi-pass
kernel). A wait=False chain remains the right tool when there's real work
to overlap with GPU execution, or the sequence is decided dynamically.
Testing
pytest tests/
test_basic.py/test_operators.py— correctness for every operator (arithmetic, scalar broadcasting, comparisons, in-place, reductions), dtype, andastypeconversion, plus error handling for mismatched buffer sizes/dtypes/devices and wrong kernel argument counts.test_async.py—wait=Falsedispatch ordering.test_buffer_reuse.py— in-place.contentswrites and repeated dispatch against the same buffers, without reallocation.test_shapes.py—.shape,.reshape(),.numpy(), and__array__, including that.contentsstays flat and elementwise ops preserve shape.test_texture.py— texture creation/shape across pixel formats and dimensionalities, upload/download roundtrips, a read-write compute kernel, and a sampling kernel (multi-texture + sampler binding).test_stability.py— repeated-dispatch and object-lifetime stress tests (regression coverage for the Metal object-ownership rules incsrc/), plus multi-threaded dispatch/compilation tests (Pipeline.runreleases the GIL, so this exercises genuinely concurrent Metal calls).test_pipeline_persistence.py— spawns separate processes to verify the on-disk pipeline binary archive is actually written and read back, and thatDevice.flush_cache()writes it on demand.
Benchmarking
python benchmarks/bench.py
Measures first-dispatch (compile-included) and steady-state warm-dispatch
latency/throughput for every operator across a range of buffer sizes, with
NumPy CPU timings alongside for context. Each run is saved as JSON
(timestamped, tagged with the git commit) under benchmarks/results/ so you
can baseline future changes:
python benchmarks/bench.py --baseline benchmarks/results/<earlier-run>.json
License
MIT
Metadata
Release files for mtlpy 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mtlpy-0.2.0.tar.gz | 229.2 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| mtlpy-0.2.0-cp314-cp314-macosx_14_0_arm64.whl | CPython 3.14 | CPython 3.14 | macOS 14.0+ ARM64 | Details |
| mtlpy-0.2.0-cp313-cp313-macosx_14_0_arm64.whl | CPython 3.13 | CPython 3.13 | macOS 14.0+ ARM64 | Details |
| mtlpy-0.2.0-cp312-cp312-macosx_14_0_arm64.whl | CPython 3.12 | CPython 3.12 | macOS 14.0+ ARM64 | Details |
| mtlpy-0.2.0-cp311-cp311-macosx_14_0_arm64.whl | CPython 3.11 | CPython 3.11 | macOS 14.0+ ARM64 | Details |
| mtlpy-0.2.0-cp310-cp310-macosx_14_0_arm64.whl | CPython 3.10 | CPython 3.10 | macOS 14.0+ ARM64 | Details |
Total release size: 1.3 MB
Release files / mtlpy-0.2.0.tar.gz
| Download URL | mtlpy-0.2.0.tar.gz |
|---|---|
| Size | 229.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3efc397c94b31ae8c2aed2dfc20eb1430c2bd254a5196ceaffc0257cf0e1ff0d
|
|
BLAKE2b-256 checksum How to use checksums |
5cf2b07c9e5ea53c2bf12ca05b08395f1e9bb28566a28f8ada7941815b8822a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / mtlpy-0.2.0-cp314-cp314-macosx_14_0_arm64.whl
| Download URL | mtlpy-0.2.0-cp314-cp314-macosx_14_0_arm64.whl |
|---|---|
| Size | 219.8 kB |
| Tags | CPython 3.14 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2d9eb9ac4ac7e61f499c239d516e16cd83ebfe5e478dacda912e7373e8a7017a
|
|
BLAKE2b-256 checksum How to use checksums |
ca3c62d295af0f0d4b7c6b9c778cf16cbeacffbef120b6ae6e238290d088c36f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / mtlpy-0.2.0-cp313-cp313-macosx_14_0_arm64.whl
| Download URL | mtlpy-0.2.0-cp313-cp313-macosx_14_0_arm64.whl |
|---|---|
| Size | 219.4 kB |
| Tags | CPython 3.13 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
1abba3c892dc83cd9e5e54f50096b4649270f9d8c83bfe001159eb4d1d458e8a
|
|
BLAKE2b-256 checksum How to use checksums |
11a14141facb20971aff0c46c07e7e7d58640a9a14f87935871533cdf66d431f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / mtlpy-0.2.0-cp312-cp312-macosx_14_0_arm64.whl
| Download URL | mtlpy-0.2.0-cp312-cp312-macosx_14_0_arm64.whl |
|---|---|
| Size | 219.4 kB |
| Tags | CPython 3.12 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9ef0ce5d004114f74cc98851803f4998f8ca554d65ecdc9c88eaa3e12ed96e5e
|
|
BLAKE2b-256 checksum How to use checksums |
55eb2ac36949e2bd30a3545e5de2b6691fa0cda56132da084706e3c561d3a1a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / mtlpy-0.2.0-cp311-cp311-macosx_14_0_arm64.whl
| Download URL | mtlpy-0.2.0-cp311-cp311-macosx_14_0_arm64.whl |
|---|---|
| Size | 218.5 kB |
| Tags | CPython 3.11 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
e512658dfc72fdabee59a78862ee202b620620d745485afd4b37160d01a3dfc6
|
|
BLAKE2b-256 checksum How to use checksums |
379591ce03505ac9388c04ce9d465728523556cb5f4b4cf1aa5cef92e683fe0a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / mtlpy-0.2.0-cp310-cp310-macosx_14_0_arm64.whl
| Download URL | mtlpy-0.2.0-cp310-cp310-macosx_14_0_arm64.whl |
|---|---|
| Size | 217.2 kB |
| Tags | CPython 3.10 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
3c2e6ff898f4cf22d926940ab8f42e5030ed716e83df9e8de3b41d0b256feb0a
|
|
BLAKE2b-256 checksum How to use checksums |
0a4cef720d8d6040e67c75f24f18c9c0912b16a7eae7222bc7950529906237ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency log