Skip to content
Back to skills

Mesh Editing And Bmesh

ASecurity

Create, edit and read meshes efficiently: when to use bpy.data, bpy.ops or bmesh, the bmesh.new / try / finally / free pattern, foreach_get and foreach_set for bulk data, and evaluated meshes for modifier results. Use when the user writes mesh-generating or mesh-editing Python, mentions bmesh, from_pydata, foreach_set or mesh.vertices, has a slow script looping over bpy.ops.mesh or per-vertex assignments, or a bmesh script that misbehaves on its second run. Targets 5.2 LTS.

  • 16 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 23, 2026
developmentpythonrustgoshellnodegitapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add TMHSDigital/Blender-Developer-Tools --skill mesh-editing-and-bmesh --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mesh Editing And Bmesh?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mesh Editing And Bmesh
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tmhsdigital-mesh-editing-and-bmesh/badge)](https://www.skillsdirectory.com/skills/tmhsdigital-mesh-editing-and-bmesh)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mesh-editing-and-bmesh
description: "Create, edit and read meshes efficiently: when to use bpy.data, bpy.ops or bmesh, the bmesh.new / try / finally / free pattern, foreach_get and foreach_set for bulk data, and evaluated meshes for modifier results. Use when the user writes mesh-generating or mesh-editing Python, mentions bmesh, from_pydata, foreach_set or mesh.vertices, has a slow script looping over bpy.ops.mesh or per-vertex assignments, or a bmesh script that misbehaves on its second run. Targets 5.2 LTS."
standards-version: 1.10.0
---

# Mesh Editing and bmesh

## Trigger

Use this skill when the user:

- Wants to create, modify, or read mesh geometry from Python
- Mentions `bmesh`, `mesh.vertices`, `foreach_set`, `from_pydata`, `evaluated_get`
- Has a script that's slow and uses `bpy.ops.mesh.*` in a loop
- Needs the mesh after modifiers (subdivision surface, mirror, geometry nodes) have been applied
- Asks why their bmesh script crashes on the second run

## Three layers, three use cases

There are three ways to touch mesh data and each is right in a specific situation.

### Layer 1: `bpy.data.meshes` and direct vertex/edge/face access

For creating new meshes from scratch or reading static geometry:

```python
import bpy

mesh = bpy.data.meshes.new("MyMesh")
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
faces = [(0, 1, 2, 3)]
mesh.from_pydata(verts, [], faces)
mesh.update()

obj = bpy.data.objects.new("MyObject", mesh)
bpy.context.scene.collection.objects.link(obj)
```

Use this layer when:

- You're constructing a mesh from a list of vertices and faces.
- You're reading vertex coordinates, normals, or face indices from an existing static mesh.
- You're doing one-shot bulk operations.

### Layer 2: `bmesh` for editable mesh structure

For surgical edits (extrude, subdivide, dissolve, build n-gons by walking edges) where you need a real edit-mode-style data structure:

```python
import bmesh

bm = bmesh.new()
try:
    bm.from_mesh(mesh)

    for v in bm.verts:
        v.co.z += 0.1

    bm.to_mesh(mesh)
    mesh.update()
finally:
    bm.free()
```

Use this layer when:

- You need adjacency (which faces share an edge, which edges meet at a vertex).
- You're doing structural edits (split edges, merge verts, dissolve faces).
- You need iteration order to be stable mid-edit.

The `try`/`finally` with `bm.free()` is **mandatory**. `bmesh.new()` allocates C-side storage that is released only when the wrapper is collected, and a traceback, global, closure or modal operator can keep it alive for the whole session. `free()` in `finally` releases it on every path and makes any later use of `bm` raise `ReferenceError`. See the rule `always-free-bmesh`.

### Layer 3: `bpy.ops.mesh.*` operators

Use only for one-shot user-facing actions, never in a loop:

```python
# Acceptable: one-shot, user just clicked something.
bpy.ops.mesh.primitive_cube_add(size=2.0)
```

```python
# WRONG: every bpy.ops call triggers a full depsgraph evaluation and UI redraw.
for obj in bpy.context.selected_objects:
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.subdivide()
    bpy.ops.object.mode_set(mode='OBJECT')
```

This is the single biggest performance trap in Blender Python. See the rule `prefer-data-over-ops-in-loops`.

## The canonical create-an-object pattern

```python
import bpy

mesh = bpy.data.meshes.new("Cube")
verts = [
    (-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 = [
    (0, 1, 2, 3), (4, 5, 6, 7),
    (0, 1, 5, 4), (1, 2, 6, 5),
    (2, 3, 7, 6), (3, 0, 4, 7),
]
mesh.from_pydata(verts, [], faces)
mesh.update()

obj = bpy.data.objects.new("Cube", mesh)
bpy.context.scene.collection.objects.link(obj)
```

Three steps:
1. Create the mesh datablock.
2. Create the object datablock pointing at the mesh.
3. Link the object into a collection so it appears in the scene.

Skipping step 3 creates the object but never shows it. The collection link is what puts it in the scene.

The deletion mirror image:

```python
obj = bpy.data.objects["Cube"]
bpy.data.objects.remove(obj, do_unlink=True)
```

`do_unlink=True` removes the object from any collections it was linked into before deleting it. It is already the default (`remove(object, *, do_unlink=True, ...)`); passing it explicitly documents intent. With `do_unlink=False`, Blender refuses to delete an object that is still referenced.

## `foreach_set` for bulk vertex injection

For setting many vertex coordinates fast, `foreach_set` is one to two orders of magnitude faster than a Python loop:

```python
import numpy as np

n_verts = len(mesh.vertices)
flat = np.empty(n_verts * 3, dtype=np.float32)
flat[0::3] = xs  # array of x coords
flat[1::3] = ys
flat[2::3] = zs

mesh.vertices.foreach_set("co", flat)
mesh.update()
```

The flat array layout `[x0, y0, z0, x1, y1, z1, ...]` is what `foreach_set` expects for `'co'`. Use `dtype=np.float32`, not `float64`; Blender expects 32-bit floats here.

`foreach_get` is the symmetric reader:

```python
flat = np.empty(n_verts * 3, dtype=np.float32)
mesh.vertices.foreach_get("co", flat)
xs = flat[0::3]
ys = flat[1::3]
zs = flat[2::3]
```

## Depsgraph evaluation: getting the modifier-applied mesh

Reading `obj.data` returns the **base** mesh, before modifiers. To read what the user sees after the modifier stack runs:

```python
depsgraph = bpy.context.evaluated_depsgraph_get()
eval_obj = obj.evaluated_get(depsgraph)
eval_mesh = eval_obj.to_mesh()

try:
    for v in eval_mesh.vertices:
        ...  # modifier-applied coords
finally:
    eval_obj.to_mesh_clear()
```

`to_mesh()` returns a temporary mesh datablock. `to_mesh_clear()` releases it. Skipping the cleanup leaks like skipping `bm.free()`.

This is the only way to:
- Read the post-subdivision-surface vertex count and positions
- Capture geometry-nodes-generated geometry from Python
- Export the mesh as the user sees it without baking modifiers first

## bmesh idioms

### Iterating in a stable order

```python
for face in bm.faces:
    if face.select:
        face.smooth = True
```

bmesh iteration is stable as long as you don't mutate the structure (add or remove elements). To mutate during iteration, copy the iterable first:

```python
for face in list(bm.faces):
    if face.calc_area() < 0.01:
        bm.faces.remove(face)
```

### `bmesh.ops`: the high-level bmesh functions

```python
import bmesh

bm = bmesh.new()
try:
    bm.from_mesh(mesh)

    bmesh.ops.subdivide_edges(
        bm,
        edges=bm.edges,
        cuts=2,
        use_grid_fill=True,
    )

    bm.to_mesh(mesh)
    mesh.update()
finally:
    bm.free()
```

`bmesh.ops.*` functions take the bmesh and keyword args. They are the Python equivalent of edit-mode operators but operate directly on the bmesh structure without round-tripping through `bpy.ops`.

## Working with selection

bmesh tracks selection via `.select` on each element:

```python
bm = bmesh.new()
try:
    bm.from_mesh(mesh)

    selected_verts = [v for v in bm.verts if v.select]
    for v in selected_verts:
        v.co.z += 0.5

    bm.to_mesh(mesh)
finally:
    bm.free()
```

After mutating selection, call `bm.select_flush_mode()` if you've changed individual element selection and want it to propagate (e.g. selecting verts should select connected edges).

## Common AI mistakes

1. **`bpy.ops.mesh.*` in a loop**. See the canonical example above. Move to `bpy.data` plus `bmesh`.

2. **Forgetting `bm.free()`**. Crashes Blender after enough runs. Use the `try`/`finally` form unconditionally.

3. **Forgetting `to_mesh_clear()`**. Leaks evaluated meshes. Same `try`/`finally` discipline applies.

4. **Reading `obj.data` and expecting modifiers**:

   ```python
   for v in obj.data.vertices:  # base mesh, no modifiers
       ...
   ```

   Use `evaluated_get(depsgraph).to_mesh()` instead.

5. **Mutating bmesh structure mid-iteration** without copying the iterable first. Crashes or skips elements.

6. **Skipping `mesh.update()`** after `foreach_set` or vertex coordinate edits. The mesh stays out of date until the next depsgraph cycle.

7. **Mismatched dtype in `foreach_set`** (a `float64` buffer for a float32 attribute). The values are converted correctly (round-trip error about 1e-7, float32 precision, on 4.5.11 and 5.2.1), but slower: 38 ms vs 21 ms for 361k vertices on 5.2.1 (34 vs 24 ms on 4.5.11). Match the attribute's type (`float32` for `co`) for bulk speed.

8. **Trusting glTF as an n-gon witness.** The exporter always triangulates. A cube and a hexagon-from-dissolved-edge both ship 12 tris. Hygiene (`len(poly.vertices) <= 4`) and `Mesh.calc_tangents` (aborts on n-gons) must run on the Blender mesh. Witness: [`examples/ngon-triangulate/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/ngon-triangulate).

9. **Treating coincident duplicate shells as non-manifold.** Two cubes occupying the same space are still valence-2. They ship as extra glTF triangles until `bmesh.ops.remove_doubles`. Witness: [`examples/coincident-vert-weld/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/coincident-vert-weld).

## Related

- `headless-batch-scripting` for using these patterns in CLI scripts
- `geometry-nodes-python` for programmatic GN tree construction
- Rule `prefer-data-over-ops-in-loops`
- Rule `always-free-bmesh`
- Snippet `canonical-object-creation.py`, `canonical-object-deletion.py`, `bmesh-load-edit-free.py`, `depsgraph-evaluated-mesh.py`, `foreach-set-vertices.py`
- Example `ngon-triangulate` for synthesized n-gons vs `calc_tangents` / triangulate
- Example `coincident-vert-weld` for authored duplicate shells vs glTF

<!-- examples:begin (generated by scripts/build_examples_index.py from examples/skills.json; do not edit) -->
## Runnable examples

Each example runs headless, asserts the contract, and exits non-zero when it breaks. Run one with `blender --background --python <script> --`; pass a falsifier flag to watch the check fail.

- [`armature-bend`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/armature-bend): Rigging end to end in the data API — edit_bones chain construction, name-bound vertex groups with smoothstep blend zones, posing, and depsgraph evaluation. Falsify: `--zero-curl` (exit 8).
- [`attribute-domain-shear`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/attribute-domain-shear): POINT vs CORNER color-attribute domains on a shared-vertex fan: CORNER stays exact per face while a naive per-wedge POINT loop shears to the last write at the hub. Falsify: `--no-overwrite` (exit 5).
- [`bmesh-gear`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/bmesh-gear): A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands. Falsify: `--no-extrude` (exit 3).
- [`boolean-exact-volume`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/boolean-exact-volume): The Boolean modifier on the EXACT solver. Falsify: `--float-solver` (exit 3).
- [`collision-hull-proxy`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/collision-hull-proxy): A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. Falsify: `--shrink-hull` (exit 3).
- [`color-attribute-wheel`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/color-attribute-wheel): The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain. Falsify: `--point-domain` (exit 5).
- [`custom-normals-shade`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/custom-normals-shade): A jerry can prop shaded three ways to prove the post-4.1 shading contract: hard edges are mesh data, landing exactly where the dihedral crosses. Falsify: `--mismatch-angle` (exit 5).
- [`degenerate-bevel-weld`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/degenerate-bevel-weld): Bevel offset >= half the min box dimension collapses the band into zero-area faces — and they ship: a stdlib GLB re-parse counts the degenerate triangles crossing the export boundary. Falsify: `--both-safe` (exit 4).
- [`lattice-deform`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lattice-deform): A 2x2x2 Lattice modifier set to KEY_LINEAR on all three axes, with two top control points moved through LatticePoint.co_deform, read back through the depsgraph (evaluated_get, to_mesh, to_mesh_clear). Falsify: `--bspline` (exit 4).
- [`lightmap-uv-channel`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lightmap-uv-channel): A market cart carrying the two-channel UV contract for baked lighting: UV0 untouched, UVLight packed with no overlaps and a respected margin. Falsify: `--overlap-islands` (exit 7).
- [`lod-decimate-chain`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lod-decimate-chain): A retro rocket at LOD0/1/2 via the Decimate modifier evaluated through the depsgraph, with a wireframe of each evaluated mesh. Falsify: `--no-decimate` (exit 6).
- [`mesh-hygiene-audit`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/mesh-hygiene-audit): Engine-ingest mesh hygiene on every part of a flanged street valve: no ngons, no loose verts, manifold edges, no zero-area faces, contiguous and outward winding, Euler V-E+F==2 on the body casting. Falsify: `--inject ngon` (exit 3).
- [`modular-kit-snap`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/modular-kit-snap): A tiling corridor kit whose open-end boundary verts snap to the tile grid, so instances at 4 m multiples join with zero gap or overlap. Falsify: `--seam-gap` (exit 5).
- [`ngon-triangulate`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/ngon-triangulate): Synthesizes one hexagon by dissolving a cube edge, then asserts the hygiene / tangent contracts on that defective mesh. Falsify: `--no-dissolve` (exit 3).
- [`shape-key-blend`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/shape-key-blend): A relative Tall shape key that turns a squat ceramic jar into a trumpet vase. Falsify: `--zero-blend` (exit 5).
- [`soccer-ball-goldberg`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/soccer-ball-goldberg): A soccer ball as a Goldberg polyhedron: a bmesh icosphere truncated at 1/3 per edge, faces ordered by link-topology walks, panels bound by face vertex count. Falsify: `--invert-bind` (exit 13).
- [`solidify-even-thickness`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/solidify-even-thickness): Solidify's use_even_offset on a folded strip. Falsify: `--no-even` (exit 4).
- [`triangulate-tangents`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/triangulate-tangents): A machined buckler verifying the tangent-space contract a game engine's normal mapping depends on. Falsify: `--zero-uv` (exit 4).
- [`uv-layer-grid`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/uv-layer-grid): The UV-layer authoring hazard — bmesh.ops.create_grid(..., calc_uvs=True) is a silent no-op unless a UV layer already exists; without one an Image Texture samples texel (0,0) everywhere. Falsify: `--precreate-on-hazard` (exit 3).
- [`vertex-color-ao`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/vertex-color-ao): A stone well carrying baked ambient occlusion in a colour attribute, with the bake held to the closed-form hemisphere integral rather than to a captured value. Falsify: `--uniform-hemisphere` (exit 3).
- [`vertex-weight-limit`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/vertex-weight-limit): A rigged industrial robot arm pruned to the game-engine cap of four bone influences per vertex, through the data API. Falsify: `--skip-limit` (exit 4).
- [`wave-displace`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/wave-displace): Bulk vertex IO at real scale — 9,409 vertices displaced into a standing wave with one foreach_get and one foreach_set, no per-vertex access. Falsify: `--flat` (exit 4).

<!-- examples:end -->

## References

- `bpy.types.Mesh`: https://docs.blender.org/api/current/bpy.types.Mesh.html
- `bmesh` module: https://docs.blender.org/api/current/bmesh.html
- `bmesh.ops`: https://docs.blender.org/api/current/bmesh.ops.html
- Depsgraph: https://docs.blender.org/api/current/bpy.types.Depsgraph.html

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.