numpy-stl and the case for a mesh library that is just NumPy
Simple library to make working with STL files (and 3D objects in general) fast and easy.
At a glance
- What is it?
- numpy-stl represents an STL mesh as plain NumPy arrays, so every geometric operation is vectorized array math and the mesh data can be handed to any other NumPy-aware tool without conversion. Its real cost is that the library is mostly a set of conventions, and the conventions are the documentation.
- Who is it for?
- numpy-stl suits a Python pipeline that already thinks in NumPy, because the mesh is a struct array of triangle vectors rather than an opaque object, so slicing, concatenating and measuring it are array operations. It does not suit a project that wants mesh repair, watertightness checks or boolean operations, since it is a file format library and not a geometry kernel.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The mesh is an array, not an object with methods
numpy-stl is a fast library for reading, writing and modifying STL files, powered by NumPy, and the key line is that every mesh operation uses vectorized array math for speed.
What that means in practice is that a mesh is data, not behaviour. The library exposes a Mesh type whose vectors are a NumPy structured array of triangle corners, and the operations that would be a method on a conventional mesh library are array operations on that array. The claim in the project description is that this makes working with STL files and 3D objects in general fast and easy, and the mechanism is that there is no conversion layer between the mesh and whatever else you want to do with the numbers.
The quick start shows the shape of it:
from stl import mesh
# Load an STL file (auto-detects binary/ASCII)
your_mesh = mesh.Mesh.from_file('model.stl')
# Inspect
print(f'{len(your_mesh)} triangles')
print(f'Bounding box: {your_mesh.min_} to {your_mesh.max_}')
# Save
your_mesh.save('output.stl')Three things are notable. from_file auto-detects whether the file is binary or ASCII, which removes the single most common source of confusion in STL handling. min_ and max_ give the bounding box directly from the array rather than through a method. And save writes the file back without you having to know which format it came from.
The performance claim is stated concretely in the project description: the Stanford Dragon, 871,414 triangles, loaded in 0.63 seconds and rendered with matplotlib.
The package is BSD-3-Clause licensed, written in Python, and the documentation is hosted on readthedocs.
One format is first class, two others are supported with caveats
The supported formats table is short and the caveats column is where the useful information sits.
STL in binary is read and written, and is auto-detected on load. STL in ASCII is read and written, with the note that it is around five times faster with the optional speedups. PLY is read and written, in both binary and ASCII, through the from_ply_file and save_ply calls. 3MF is read but not written, and is marked experimental, with the from_3mf_file entry point.
The asymmetry between STL and the others is not accidental. STL is the format the library is named for, and it is the one with the simplest structure, a flat list of triangles with no index, no hierarchy and no scene graph. That flatness is what makes the vectorized approach work so well, and it is also why STL is awkward for anything other than a single solid body.
PLY carries more structure and is fully supported for both directions, which makes it the sensible interchange format if your pipeline needs to round-trip. 3MF is the interesting one, because it is the modern format for 3D printing and is a reasonable thing to want, and the project treats it as read-only and experimental.
Note the load call for 3MF in the API cheatsheet returns a list: list(mesh.Mesh.from_3mf_file('m.3mf')), rather than a single mesh. A 3MF file can contain multiple objects, so the shape of the return value is different from every other format.
The optional Cython extension buys five times on ASCII only
The performance section is the most honest part of the documentation, because it says where the speedup is and where it is not.
The library is fast out of the box. For even faster ASCII STL I/O you install the optional Cython speedups:
pip install numpy-stl[fast]That installs the speedups package, a compiled C extension for ASCII parsing. The library works identically without it, and pure Python is the default.
The benchmark is quoted as a median of five runs, and the table is the interesting part:
| Facets | Pure Python | Speedups | Factor | |----------:|-------------:|-----------:|-------:| | 10,000 | 36 ms | 7 ms | 5.1x | | 100,000 | 0.36 s | 73 ms | 4.9x | | 871,414 | 3.10 s | 0.59 s | 5.2x | | 1,000,000 | 3.60 s | 0.73 s | 4.9x |
The factor is consistent across three orders of magnitude, which is what you would expect from replacing a Python-level parser with a C one. But note what the columns do not say: this is ASCII read performance only. Binary STL is already a bulk array read, and the vectorized NumPy path handles it without a per-line parser, so there is nothing for the C extension to do there.
That is worth understanding before installing anything. If your pipeline reads binary STL, the extension buys you nothing. If it reads ASCII STL, and ASCII STL is what a lot of CAD exports and every hand-rolled generator produce, it is a five times improvement on the most expensive part of the pipeline.
The documentation also notes that results will vary by hardware, and gives you the command to run the benchmark yourself: python benchmarks/benchmark_ascii_read.py.
The operations are the ones you would write with NumPy anyway
The feature list covers mesh operations, properties, combination, and command line tools, and the examples show how little ceremony each takes.
Rotating and translating a mesh is two calls on the loaded object:
import math
from stl import mesh
m = mesh.Mesh.from_file('model.stl')
m.rotate([0, 0, 1], math.radians(90))
m.translate([10, 0, 0])
m.save('transformed.stl')Properties come from a single call that returns the three values at once:
from stl import mesh
m = mesh.Mesh.from_file('closed_model.stl')
volume, cog, inertia = m.get_mass_properties()
print(f'Volume: {volume:.4f}')
print(f'Center of gravity: {cog}')Combining meshes is concatenation of the underlying arrays, which is the clearest statement of the design:
import numpy as np
from stl import mesh
m1 = mesh.Mesh.from_file('part1.stl')
m2 = mesh.Mesh.from_file('part2.stl')
combined = mesh.Mesh(np.concatenate([m1.data, m2.data]))
combined.save('combined.stl')There is no combine method with a name and options. You concatenate the data arrays and construct a new mesh. That is the library's whole thesis in three lines, and it is a reason to like the library if you already know NumPy, and a reason to find it bare if you do not.
The property list is worth noting for what it does not include. Surface area, volume, center of gravity, inertia tensor and convexity are there. Repair, watertightness, boolean operations, smoothing and decimation are not, and they are not what this library is for.
Building a mesh from scratch means filling an array by hand
Creating a mesh programmatically is the one case where the array-first design costs you a little effort, and the example is instructive.
You define the vertices and the faces as NumPy arrays, then fill a preallocated mesh:
import numpy as np
from stl import mesh
# Define vertices and faces of a cube
vertices = np.array([
[-1, -1, -1], [+1, -1, -1], [+1, +1, -1], [-1, +1, -1],
[-1, -1, +1], [+1, -1, +1], [+1, +1, +1], [-1, +1, +1],
])
faces = np.array([
[0, 3, 1], [1, 3, 2], [0, 4, 7], [0, 7, 3],
[4, 5, 6], [4, 6, 7], [5, 1, 2], [5, 2, 6],
[2, 3, 6], [3, 7, 6], [0, 1, 5], [0, 5, 4],
])
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
for j in range(3):
cube.vectors[i][j] = vertices[f[j], :]
cube.save('cube.stl')The mesh is allocated with the right number of triangles and the right dtype, and then each corner is written by index. There is no add_vertex call, which means there is no per-element Python overhead, and there is also no validation that your face indices are in range, which means a mistake in the faces array produces a corrupt file rather than an exception.
For a library whose pitch is speed, that trade is coherent. For a user who wants to build geometry programmatically and is used to an API that checks its arguments, it is a rougher surface than expected.
Command line tools, plotting, and the package metadata
The library ships three console scripts, which the project metadata registers as stl, stl2ascii and stl2bin, all pointing at stl.main. That covers the format conversion case without writing a script, and the feature list names them as CLI tools for format conversion.
Plotting is done by handing the vectors straight to matplotlib, and the example is short enough to be the whole story:
import math
from stl import mesh
from mpl_toolkits import mplot3d
from matplotlib import pyplot
figure = pyplot.figure(figsize=(8, 6))
axes = figure.add_subplot(projection='3d')
dragon = mesh.Mesh.from_ply_file('dragon_vrip.ply')
dragon.rotate([1, 0, 0], math.radians(-90))
axes.add_collection3d(
mplot3d.art3d.Poly3DCollection(dragon.vectors)
)
scale = dragon.points.flatten()
axes.auto_scale_xyz(scale, scale, scale)
pyplot.show()Note that the mesh is loaded from PLY here, the vectors go straight into a Poly3DCollection, and the axis scaling is done with the flattened points. There is no plotting helper in the library, because there does not need to be one.
The package metadata is consistent with a mature, typed library. The build uses uv_build, the project version is 4.1.1, and the classifiers mark it as Development Status 6, Mature, with a Typing :: Typed classifier and a typings/ directory in the tree. The dependencies are numpy at 1.24 or newer, python-utils, and typing_extensions, with comments in the manifest explaining that numpy needs 1.24 because np.fromiter with a structured dtype requires 1.23 and 1.24 is the oldest version in the test matrix, and that typing_extensions is declared explicitly because stl.stl imports assert_never at runtime.
The repository also carries a SECURITY.md, a CHANGELOG.md, a benchmarks/ directory, a lefthook.yml, a ruff.toml, a tox.ini and a conftest.py, with the default branch being develop.
Editorial conclusion
numpy-stl suits a Python pipeline that already thinks in NumPy, because the mesh is a struct array of triangle vectors rather than an opaque object, so slicing, concatenating and measuring it are array operations. It does not suit a project that wants mesh repair, watertightness checks or boolean operations, since it is a file format library and not a geometry kernel. Before you adopt it, read the benchmark table rather than the headline number, because the speedup only applies to ASCII parsing and only with the optional Cython extension installed, and note that the last push was on 2026-10-19 with v4.1.1 published the same day.
Frequently asked questions
How do I install numpy-stl and load a mesh?
Run pip install numpy-stl, then load with mesh.Mesh.from_file('model.stl'), which auto-detects binary or ASCII. The mesh exposes len() for the triangle count and min_ and max_ for the bounding box, and save() writes the file back.
What file formats does numpy-stl support?
STL in binary and ASCII are both read and written, with the binary form auto-detected on load. PLY is read and written in binary and ASCII through from_ply_file and save_ply. 3MF is read only, marked experimental, and from_3mf_file returns a list rather than a single mesh.
What does the optional Cython speedups package actually speed up?
ASCII STL parsing. Installing numpy-stl[fast] brings in the speedups package, a compiled C extension, and the quoted benchmark is about five times faster on ASCII reads at every size tested. Binary STL is already a bulk array read and gains nothing from it, and the library works identically without the extension.
How do I combine two meshes in numpy-stl?
By concatenating the underlying data arrays and constructing a new mesh, for example mesh.Mesh(np.concatenate([m1.data, m2.data])). There is no dedicated combine method, which reflects the design of keeping the mesh as plain NumPy data.
Does numpy-stl do mesh repair or boolean operations?
No. The property list covers surface area, volume, center of gravity, inertia tensor and convexity, and the operations cover rotate, translate and a 4x4 transform. Repair, watertightness checks, smoothing and decimation are outside what this library does, since it is a file format library rather than a geometry kernel.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/wolph-numpy-stl)Was this useful?