If you have ever used Python, you would have definitely come across the with statement. It’s widely used when reading from or writing to files on the disk. But, ever wondered what are we getting out of using the with statement exactly?

with open('some_file.ext', 'w') as f:
    f.write('some_content')

The with statement allows us to create a context manager (not related to bpy.context in Blender) in Python. Context managers solve the problem of ‘setup’ and ‘teardown’. The ‘setup’ is done when you enter into a with block and the ‘teardown’ is done when you leave its scope.

In case of files, they open a file for reading or writing, let you do something with the file and once you are done, they automatically close the file, freeing up the memory for other processes.

But with statements are not limited to files. They can be implemented for a wide variety of use cases where there is some kind of ‘setup’ and ‘teardown’ involved, and some standard library modules like the socket module implement their own such context managers.

Where context managers are useful in Blender

In Blender, certain operations can be done only in Edit Mode. Let’s say you have a mesh and you want to perform a series of the operations: Merge by Distance, Limited Dissolve, and finally Tris to Quads. In order to do this, you will have to first select the object, switch to Edit Mode, perform these operations, and finally switch back to Object Mode once you are done.

If you were to do it the usual way using the API, the process would go something like this:

# Switch to object mode (if not already)
bpy.ops.object.mode_set(mode='OBJECT')

# Ensure no other object is selected
bpy.ops.object.select_all(action='DESELECT')

# Ensure the object we want to edit is the active object
bpy.context.view_layer.objects.active = bpy.data.objects['Cube']

# Switch to Edit Mode
bpy.ops.object.mode_set(mode='EDIT')

# Perform the mesh operations
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_limited()
bpy.ops.mesh.tris_convert_to_quads()

#Switch back to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')

If you notice, there’s a ‘setup’ involved here which is everything that we do before performing the mesh operations – ensuring object mode, deselecting everything, making the correct object active, switching to object mode – which is kind of similar to choosing a file path and opening the file for writing. Then you do some mesh operations, which is equivalent to reading from/writing to the file. Then eventually you leave Edit Mode, similar to closing a file, which can be considered as our ‘teardown’ step.

And you have to do this every time you want to perform some operations on a particular mesh. Context managers take care of these repetitive setup and teardown processes and let you focus on doing what you wanted to do.

Let’s imagine a syntax for our Edit Mode based mesh operations similar to how we use the with statement for reading/writing files:

with EditMode(mesh_object):
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.remove_doubles()
    bpy.ops.mesh.dissolve_limited()
    bpy.ops.mesh.tris_convert_to_quads()

Doesn’t that look cleaner? All that hassle of ensuring the we switch to Object Mode, deselecting unnecessary objects, ensuring our object is active, switching to Edit Mode and the final step of switching back to Object mode has been delegated to the Edit Mode context manager.

Of course, this syntax doesn’t exist yet, but that is what we are gonna do.

Implementing an Edit Mode context manager

Typically, to implement our own context manager, we have to write a class that implements an __enter__() and an __exit__() method. The former will take care of the setup process and the latter will take care of the teardown. When you initiate a context manager, the __enter__() would run first to set this up and would yield the execution to the caller’s context, so that any statements within the with block can be executed and then finally the __exit__() method is executed to tear things down.

But Python provides a simpler way to implement context managers, without having to write classes, through the contextlib standard library. This library provides a decorator named contextmanager that we can use on a function to turn it into a context manager. We can use this decorator to write a context manager like this:

import bpy
import contextlib

@contextlib.contextmanager
def EditMode(active_object):
    # Setup process
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='DESELECT')
    bpy.context.view_layer.objects.active = active_object
    bpy.ops.object.mode_set(mode='EDIT')
    
    try:
        # Yielding execution to the outer context to execute
        # the statements inside the 'with' block
        yield
    finally:
        # Teardown process
        bpy.ops.object.mode_set(mode='OBJECT')

There are a few of things worth noting here:

  1. Anything we put immediately after the function header will be run during the setup process. So in this case, we take of preparing things and switching to Edit Mode.
  2. Inside the try block we usually yield something to the outer context. If you relate this with the files example, whatever we yield here will be stored in the variable that we put after the as keyword. But since we don’t need any particular object to deal with (since we are calling bpy.ops.mesh methods directly) we simply put an yield statement. The yield keyword tells the context manager to execute the statements inside the with block.
  3. Anything we put inside the finally block will executed once all the statements inside the with block have been executed and essentially form the teardown step.

Now we can perform our mesh operations like this:

with EditMode(mesh_object):
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.remove_doubles()
    bpy.ops.mesh.dissolve_limited()
    bpy.ops.mesh.tris_convert_to_quads()

All of those steps before and after our mesh operations will be taken care by our context manager. Feel free to select a different object other than the mesh_object and see if our context manager handles everything correctly.

Implementing a BMesh context manager

Now, even though the above example explains how a context manager can be implemented for our own use cases, this isn’t the best way to perform mesh operations in Blender. Usually bpy.ops methods are intended to be called from the UI and there are better alternative ways to perform the same predictably when doing things via the API.

For example, we can perform our mesh operations without having to depend on Edit Mode using the bmesh module. That would look something like this:

import bmesh

# Create a new BMesh object
mesh = bmesh.new()

# Add the mesh data from our object
mesh.from_mesh(mesh_object.data)

# Perform the mesh operations
bmesh.ops.remove_doubles(mesh, verts=mesh.verts, dist=0.0001)

bmesh.ops.dissolve_limit(
    mesh,
    verts=mesh.verts,
    edges=mesh.edges,
    angle_limit=0.0872665
)

bmesh.ops.join_triangles(
    mesh, 
    faces=mesh.faces,
    angle_face_threshold=0.698132,
    angle_shape_threshold=0.698132
)

# Nothing would change yet, since we have to apply the changes
# back to our original object
mesh.to_mesh(mesh_object.data)

# Free the BMesh object
mesh.free()

Again, if you look at the above code, before we start performing our mesh operations, we do some setup. In this case we create a new bmesh object and get the mesh data from our mesh_object. Similarly, once we finish our mesh operations, we do some teardown by assigning the mesh data back to our original mesh_object and freeing the bmesh object from memory.

This setup and teardown has to be done every time we want to perform some mesh operations.

And, naturally, we can package this up into a context manager. In this case, we have to indeed yield something back to the calling context:

import bpy
import bmesh
import contextlib

@contextlib.contextmanager
def BMeshObject(mesh_object):
    # Setup process
    mesh = bmesh.new()
    mesh.from_mesh(mesh_object.data)
    
    try:
        # Yielding mesh to the outer context to execute
        # the statements inside the 'with' block
        yield mesh
    finally:
        # Teardown process
        mesh.to_mesh(mesh_object.data)
        mesh.free()

Note how we are implementing similar setup and teardown processes as our last example and also yield the mesh to the outer context for further execution. Now we can perform our mesh operations like so:

with BMeshObject(mesh_object) as mesh:
    bmesh.ops.remove_doubles(mesh, verts=mesh.verts, dist=0.0001)

    bmesh.ops.dissolve_limit(
        mesh,
        verts=mesh.verts,
        edges=mesh.edges,
        angle_limit=0.0872665
    )

    bmesh.ops.join_triangles(
        mesh, 
        faces=mesh.faces,
        angle_face_threshold=0.698132,
        angle_shape_threshold=0.698132
    )

Where the mesh next to the as keyword will be assigned the mesh object that we yield from our context manager. The creation of the bmesh object, assigning our object’s mesh to it, assigning back the modified mesh to our object and freeing the bmesh object from memory – all of that will be handled by our context manager!

Note: If you are wondering about those parameter values for angle_limit, angle_face_threshold, etc. I copied those values from the equivalent bpy.ops.mesh operators.

For some reason, the bmesh operators do not share the same defaults, or even variable names, even though the bpy.ops.mesh operators call the bmesh operators under the hood.

Closing thoughts

Context managers can be applied to a wide variety of use cases where there is some kind of setup and teardown involved. And they can help us greatly eliminate repetitive boilerplate from our code. Here are a few other use cases for context managers:

  • Creating a Boolean context manager where operators like +, -, & can be used to perform mesh boolean operations. E.g. obj1 + obj2, obj1 - obj2, etc.
  • Creating a OnlyActive **context manager **which takes an object as input and creates a context where that is the only active and selected object.
  • Creating a Transform context manager, which takes an object as input and performs any transformations (move, rotate, scale, etc.) to any object along the local axis of the input object within that context

Do you wish something else can make a great context manager. Let me know!

Resources