Tutorials
October 31, 2026
pythoniostlobjtutorial

Fast STL and OBJ Reading in Python

Read and write STL and OBJ mesh files in Python straight into NumPy arrays. A 1M-triangle OBJ in 6.6 ms and a welded binary STL in 36.4 ms, measured against PyVista, trimesh and meshio. One pip install.

Žiga Sajovic, Polydera

Also available in C++ and JavaScript.

Most mesh work in Python starts with reading a file, and on large meshes the reader can take longer than the work. trueform reads STL and OBJ straight into NumPy arrays: an (N, 3) face array and an (M, 3) point array. STL triangles are welded into shared vertices as they load. One pip install. NumPy arrays in, NumPy arrays out. This tutorial covers reading and writing both formats, triangles, quads and mixed polygons, index and point dtypes, writing with a transformation, and measured read times against PyVista, trimesh and meshio.

trueform GitHub Documentation

Install

pip install trueform

See the installation guide for all options.

Reading STL

import numpy as np
import trueform as tf

faces, points = tf.read_stl("model.stl")
# faces:  (N, 3) int32
# points: (M, 3) float32

An STL file stores three corners per triangle with no sharing. tf.read_stl merges corners at the same position, so points holds each vertex once and faces indexes into it. The result is a connected mesh, ready for topology queries.

Wrap the arrays in a tf.Mesh to use them with the rest of the library:

mesh = tf.Mesh(*tf.read_stl("model.stl"))
print(mesh)     # Mesh(... points, ... faces, 3-gon, 3D, dtype=float32)

For meshes with more than 2 billion vertices, ask for int64 indices:

faces, points = tf.read_stl("model.stl", index_dtype=np.int64)

Reading OBJ

OBJ files may mix triangles, quads and larger polygons. By default tf.read_obj returns the faces as an OffsetBlockedArray, one block per face:

faces, points = tf.read_obj("model.obj")
print(type(faces).__name__)         # OffsetBlockedArray
print(faces[0])                     # vertex ids of face 0

If you know the face size, ask for a fixed-width array:

tri_faces, points = tf.read_obj("model.obj", ngon=3)    # (N, 3) int32

The point dtype is float32 by default. Pass dtype=np.float64 for double precision, and index_dtype=np.int64 for large meshes:

faces, points = tf.read_obj("model.obj", ngon=3, dtype=np.float64, index_dtype=np.int64)

Quads and mixed polygons

A small quad mesh shows the two forms:

with open("quads.obj", "w") as f:
    f.write("v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nv 2 0 0\nv 2 1 0\n")
    f.write("f 1 2 3 4\nf 2 5 6 3\n")

quads, points = tf.read_obj("quads.obj", ngon=4)
print(quads)                        # [[0 1 2 3] [1 4 5 2]]

poly_faces, points = tf.read_obj("quads.obj")
quad_mesh = tf.Mesh(poly_faces, points)
print(quad_mesh)                    # Mesh(6 points, 2 faces, dynamic, 3D, dtype=float32)

OBJ indices start at 1; the arrays start at 0. To get triangles from polygons, use tf.triangulated:

tri_faces, tri_points = tf.triangulated((poly_faces, points))
print(tri_faces.shape)              # (4, 3)

Writing STL and OBJ

Both writers take a (faces, points) tuple or a tf.Mesh and return True on success:

faces, points = tf.read_stl("model.stl")

tf.write_stl((faces, points), "out.stl")
tf.write_obj((faces, points), "out.obj")
tf.write_obj(quad_mesh, "quads_out.obj")    # polygons stay polygons in OBJ

STL holds triangles only. OBJ keeps quads and mixed polygons as they are.

Writing with a transformation

Pass a 4×4 matrix to place the mesh as it is written. The arrays are not changed.

T = np.eye(4, dtype=np.float32)
T[:3, 3] = [0.0, 0.0, 5.0]          # move up by 5

tf.write_stl((faces, points), "moved.stl", transformation=T)

A transformation set on a tf.Mesh is applied too:

mesh = tf.Mesh(faces, points)
mesh.transformation = T
tf.write_stl(mesh, "moved.stl")

Working with results

The arrays are plain NumPy, so they go straight into any NumPy code or into the rest of trueform: booleans, decimation, hole filling, connected components. A round trip keeps the data:

faces, points = tf.make_sphere_mesh(1.0, stacks=100, segments=200)
tf.write_obj((faces, points), "sphere.obj")

faces2, points2 = tf.read_obj("sphere.obj", ngon=3)
print(np.array_equal(faces, faces2), np.allclose(points, points2))    # True True

Performance

Read times on a 1,025,766-triangle mesh, in Python. OBJ is read as triangles into float32 points and int32 faces. Binary STL is read and welded exactly into shared vertices. Times include opening the file, parsing, and building the output arrays.

6.6msOBJ read1,025,766 triangles
36.4msSTL read and weld1,025,766 triangles
22×faster OBJ than PyVista/VTK144 ms

Apple M4 Max 16 threads

LibraryOBJSTLtrueform6.6 ms36.4 msPyVista/VTK144 ms53.3 mstrimesh1,039 ms1,387 msmeshio1,296 ms1,770 ms

Of the four readers measured, trueform is the fastest on both formats: 22× PyVista/VTK, 158× trimesh and 197× meshio on OBJ; 1.5× PyVista/VTK, 38× trimesh and 49× meshio on STL. Median of three fresh-process medians. Benchmark run with trueform 0.10.3, PyVista 0.48.4 / VTK 9.5.2, trimesh 5.1.0 and meshio 5.3.5.

Full benchmarks · Documentation

Cite as
@article{polydera:fast-stl-and-obj-reading-in-python,
  title={Fast STL and OBJ Reading in Python},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/fast-stl-and-obj-reading-in-python},
  organization={Polydera}
}