# build123d Tutorial: Model a NEMA 17 Motor Bracket in Python

build123d tutorial: model a parametric NEMA 17 motor bracket in Python with selectors, fillets, holes, and slots, then export STEP. Full script included.

Written by [Nico Tzovanis](https://www.flux.ai/nico)

Published Nov 2026 - 13 min read

![build123d Tutorial: Model a NEMA 17 Motor Bracket in Python](https://www.flux.ai/p/assets/blogs/build123d-python-cad-motor-bracket/main.webp)

Flux is an AI hardware engineer. Describe a part in plain language, and Flux writes the parametric CAD code for it, builds the exact solid, and exports STEP for machining. In the same project, Flux designs the electronics and the enclosure around the part. This post shows it with one real part: an aluminum bracket for a NEMA 17 stepper motor, with slots that let you slide the motor to tension a belt.

To write the code yourself, follow the build123d tutorial after the Flux section. build123d is an open-source Python library for parametric CAD. You write a part as Python code, and build123d builds an exact solid that you export as STEP or STL. The tutorial covers the features you use on almost every part: builders, workplanes, selectors, fillets, holes, patterns, and export. The complete script is at the end. It runs as it is with build123d 0.11.

## Let Flux model the bracket for you

Flux designs the mechanical parts and the electronics of a device in one project. You describe the part in plain language, and Flux writes the parametric CAD code for it, the same kind of code that this tutorial writes by hand. Flux builds exact solids with extrudes, revolves, sweeps, lofts, shells, fillets, chamfers, holes, and patterns, and it keeps the part editable.

-   **Editable CAD code with named parameters.** Ask Flux to make plate thickness, hole spacing, and shaft height parameters. You can change a value yourself or ask Flux to change it, and Flux rebuilds the part.
-   **Bring your own STEP.** Drop the STEP file from your build123d script into a Flux project, and Flux adds it as a model asset next to the board and the other parts.
-   **Stress and deflection.** Ask Flux to analyze the bracket under the belt load. It shows where the stress is highest and suggests a geometry change, for example a thicker plate or a larger gusset.
-   **The rest of the device.** In the same project, Flux designs the motor driver board, places its connector and mounting holes, and fits the board and the bracket into the enclosure.
-   **Manufacturing outputs.** Flux exports the part as STEP, STL, 3MF, or BREP, makes a dimensioned drawing, and makes CNC toolpaths and G-code for your mill.

See what Flux does for mechanical design on the [Flux MCAD page](https://www.flux.ai/p/mcad).

## Prompts to try in Flux

-   “Design a 6061 aluminum bracket for a NEMA 17 motor. Put the shaft 30 mm above the mounting surface, add two gussets, and add two M5 slots with 10 mm of travel for belt tension. Make thickness, shaft height, and slot length parameters.”
-   “Analyze stress and deflection in this motor bracket under a 20 N load at the mounting face. Summarize the result and suggest a geometry change.”
-   “Design a stepper driver board for this motor with a 24 V input and a 4-pin motor connector. Fit the board next to the bracket and add its mounting holes.”
-   “Change the bracket for a NEMA 23 motor and keep the slots in the same place.”
-   “Make the CNC toolpaths for this bracket on my 3-axis mill and give me the G-code file and a toolpath preview.”

[Start Free Trial](https://www.flux.ai/signup)

## What build123d is

build123d grew out of the CadQuery community. Both libraries use the same OpenCascade kernel through Python, so both produce exact boundary-representation (BREP) solids, not meshes. The difference is the way you write code:

-   **Builder mode** uses Python `with` blocks. `BuildPart`, `BuildSketch`, and `BuildLine` collect the objects that you create inside them. This tutorial uses builder mode, because it reads like a feature tree in a CAD tool.
-   **Algebra mode** uses operators on objects, for example `part = Box(50, 45, 5) + Cylinder(10, 30)` and `part -= hole`. It suits short scripts and generated geometry.

Install it with `pip install build123d`. To see the part while you write it, install the OCP CAD Viewer extension for VS Code and call `show(part)` from `ocp_vscode`. All units are millimeters.

## The part: a NEMA 17 motor bracket

Start with the requirements, not the code. The bracket must:

-   Fit a NEMA 17 motor: a 42.3 mm square face, four M3 screws on a 31 mm square pattern, and a 22 mm pilot boss.
-   Hold the motor shaft 30 mm above the surface that the bracket bolts to.
-   Bolt down with two M5 screws in slots, so you can slide the motor 10 mm to tension a belt.
-   Be stiff enough for belt tension: 5 mm thick 6061 aluminum with two gussets at the bend.
-   Leave room for the screw heads and for a pulley on the shaft.

The motor bolts to the back face of an upright plate. Its shaft goes through the plate, and the pulley sits above the base plate. The base extends to the front, where the slots are.

![Isometric view of the NEMA 17 motor bracket built with build123d: an L-shaped aluminum bracket with a large pilot hole and four M3 holes in the upright plate, two triangular gussets at the bend, and two slots in the base plate](https://www.flux.ai/p/assets/blogs/build123d-python-cad-motor-bracket/bracket-iso.webp)

The finished bracket, exported from the build123d script below.

## Step 1: Put every dimension in a named parameter

A parametric part is only as useful as its parameters. Give each value that you expect to change a name, and derive other values from it. Here the height of the upright follows from the shaft height and the motor size, so a different motor gives a correct upright automatically.

```python
from build123d import *

# NEMA 17 motor interface
motor_face = 42.3        # square face of the motor, mm
hole_spacing = 31.0      # M3 mounting-hole pattern
pilot_clearance = 22.5   # 22 mm pilot boss + 0.5 mm clearance
screw_clearance = 3.4    # M3 normal-fit clearance hole

# Bracket
thickness = 5.0
width = 50.0
base_depth = 45.0
shaft_height = 30.0      # shaft axis above the mounting surface
upright_height = shaft_height + motor_face / 2 + 4
inner_radius = 4.0
corner_radius = 5.0
gusset_size = 20.0
gusset_thickness = 4.0
slot_pitch = 30.0
slot_length = 10.0       # belt-tension travel
slot_width = 5.5         # M5 clearance
```

The shaft height of 30 mm is not arbitrary. The lower motor screws are 15.5 mm below the shaft. At a lower shaft height, their heads would hit the inside fillet at the bend.

## Step 2: Make the two plates

`BuildPart` starts a part. Each object that you create inside it is added to the part. The `align` argument sets which point of the box sits at the origin. With `Align.MIN` on Y and Z, both plates start at the back edge and at the mounting surface, so they overlap at the bend and fuse into one solid.

```python
with BuildPart() as bracket:
    # Base plate, lying on the mounting surface
    Box(width, base_depth, thickness, align=(Align.CENTER, Align.MIN, Align.MIN))
    # Upright plate that carries the motor
    Box(width, thickness, upright_height, align=(Align.CENTER, Align.MIN, Align.MIN))
```

## Step 3: Use selectors to find the edges to fillet

Selectors are the core skill in build123d. You do not click an edge. You describe it. `bracket.edges()` returns every edge of the part as a `ShapeList`, and you narrow the list down:

-   `filter_by(Axis.X)` keeps only the edges that run parallel to the X axis.
-   `filter_by_position(Axis.Z, min, max)` keeps the edges at a height between two values.
-   `sort_by(Axis.Y)` sorts the edges from back to front.
-   `group_by(Axis.Y)` makes groups of edges at the same Y position. `[-1]` takes the group at the largest Y.

```python
    # Round the inside bend: the edge along X where the two plates meet
    bend = (
        bracket.edges()
        .filter_by(Axis.X)
        .filter_by_position(Axis.Z, thickness, thickness)
        .sort_by(Axis.Y)[0]
    )
    fillet(bend, radius=inner_radius)

    # Round the outside corners of the base and of the upright
    base_corners = bracket.edges().filter_by(Axis.Z).group_by(Axis.Y)[-1]
    top_corners = bracket.edges().filter_by(Axis.Y).group_by(Axis.Z)[-1]
    fillet(base_corners + top_corners, radius=corner_radius)
```

Two edges run along X at the height of the base top: the bend, and the front edge of the base. Sorted by Y, the bend comes first. The inside fillet removes the sharp corner where stress collects when the belt pulls on the motor.

Do the fillets before you add holes and gussets. At this point the part has few edges, so each selector finds exactly the edges you mean. After you cut the holes, the same selector would also find the edges of the holes.

## Step 4: Sketch and extrude the gussets

A gusset is a triangle in the YZ plane. `Plane.YZ.offset(x)` moves the workplane to the X position of each gusset. On that plane, the local coordinates are (Y, Z). `BuildLine` draws the closed outline, `make_face()` turns it into a face, and `extrude(..., both=True)` gives it thickness on both sides of the plane.

```python
    # Two gussets stiffen the bend
    for x in (-(width - gusset_thickness) / 2, (width - gusset_thickness) / 2):
        with BuildSketch(Plane.YZ.offset(x)):
            with BuildLine():
                Polyline(
                    (thickness, thickness),
                    (thickness + gusset_size, thickness),
                    (thickness, thickness + gusset_size),
                    close=True,
                )
            make_face()
        extrude(amount=gusset_thickness / 2, both=True)
```

The gussets sit at the outer edges of the bracket, so they stay clear of the motor screw heads and of the pulley in the middle.

## Step 5: Cut the motor holes on a workplane

`Hole` cuts in the negative Z direction of the current location. To cut through the upright, define a plane on the motor seat with its Z axis pointing out of the back face, toward the motor. Then `Locations(seat)` puts the pilot hole on the shaft axis, and `GridLocations` inside it puts the four screw holes on the 31 mm square pattern around that axis.

```python
    # Motor seat: the back face of the upright, centered on the shaft axis
    seat = Plane(origin=(0, 0, shaft_height), x_dir=(1, 0, 0), z_dir=(0, -1, 0))
    with Locations(seat):
        Hole(radius=pilot_clearance / 2)
        with GridLocations(hole_spacing, hole_spacing, 2, 2):
            Hole(radius=screw_clearance / 2)
```

Without a `depth`, `Hole` cuts through the whole part. The 22.5 mm hole clears the motor's pilot boss, which centers the motor on the plate.

## Step 6: Cut the adjustment slots

Sketch two slots on the top face of the base and subtract them. `SlotCenterToCenter` takes the distance between the two arc centers, which is the travel you get, and the slot width. `rotation=90` turns the slots to run front to back, the direction in which the belt pulls. `Mode.SUBTRACT` makes the extrusion a cut.

```python
    # Slotted holes in the base let you slide the motor to tension a belt
    with BuildSketch(Plane.XY.offset(thickness)):
        with Locations((-slot_pitch / 2, base_depth - 12), (slot_pitch / 2, base_depth - 12)):
            SlotCenterToCenter(slot_length, slot_width, rotation=90)
    extrude(amount=-thickness, mode=Mode.SUBTRACT)
```

## Step 7: Check the part and export STEP

`bracket.part` is the finished solid. Check its size and mass before you send it anywhere, then export it.

```python
part = bracket.part
bb = part.bounding_box()
print(f"Size: {bb.size.X:.1f} x {bb.size.Y:.1f} x {bb.size.Z:.1f} mm")
print(f"Volume: {part.volume:.0f} mm^3, mass in 6061 aluminum: {part.volume * 2.70e-3:.0f} g")

export_step(part, "nema17_bracket.step")
export_stl(part, "nema17_bracket.stl")
```

The script prints:

```
Size: 50.0 x 45.0 x 55.1 mm
Volume: 22467 mm^3, mass in 6061 aluminum: 61 g
```

Use the STEP file for a machining quote or to continue the part in another CAD tool. It keeps the exact cylinders, fillets, and planes. Use the STL file for 3D printing, where a mesh is enough.

## The complete build123d script

Copy this into a file and run it with Python. It writes the STEP and STL files to the current folder.

```python
from build123d import *

# NEMA 17 motor interface
motor_face = 42.3        # square face of the motor, mm
hole_spacing = 31.0      # M3 mounting-hole pattern
pilot_clearance = 22.5   # 22 mm pilot boss + 0.5 mm clearance
screw_clearance = 3.4    # M3 normal-fit clearance hole

# Bracket
thickness = 5.0
width = 50.0
base_depth = 45.0
shaft_height = 30.0      # shaft axis above the mounting surface
upright_height = shaft_height + motor_face / 2 + 4
inner_radius = 4.0
corner_radius = 5.0
gusset_size = 20.0
gusset_thickness = 4.0
slot_pitch = 30.0
slot_length = 10.0       # belt-tension travel
slot_width = 5.5         # M5 clearance

with BuildPart() as bracket:
    # Base plate, lying on the mounting surface
    Box(width, base_depth, thickness, align=(Align.CENTER, Align.MIN, Align.MIN))
    # Upright plate that carries the motor
    Box(width, thickness, upright_height, align=(Align.CENTER, Align.MIN, Align.MIN))

    # Round the inside bend: the edge along X where the two plates meet
    bend = (
        bracket.edges()
        .filter_by(Axis.X)
        .filter_by_position(Axis.Z, thickness, thickness)
        .sort_by(Axis.Y)[0]
    )
    fillet(bend, radius=inner_radius)

    # Round the outside corners of the base and of the upright
    base_corners = bracket.edges().filter_by(Axis.Z).group_by(Axis.Y)[-1]
    top_corners = bracket.edges().filter_by(Axis.Y).group_by(Axis.Z)[-1]
    fillet(base_corners + top_corners, radius=corner_radius)

    # Two gussets stiffen the bend
    for x in (-(width - gusset_thickness) / 2, (width - gusset_thickness) / 2):
        with BuildSketch(Plane.YZ.offset(x)):
            with BuildLine():
                Polyline(
                    (thickness, thickness),
                    (thickness + gusset_size, thickness),
                    (thickness, thickness + gusset_size),
                    close=True,
                )
            make_face()
        extrude(amount=gusset_thickness / 2, both=True)

    # Motor seat: the back face of the upright, centered on the shaft axis
    seat = Plane(origin=(0, 0, shaft_height), x_dir=(1, 0, 0), z_dir=(0, -1, 0))
    with Locations(seat):
        Hole(radius=pilot_clearance / 2)
        with GridLocations(hole_spacing, hole_spacing, 2, 2):
            Hole(radius=screw_clearance / 2)

    # Slotted holes in the base let you slide the motor to tension a belt
    with BuildSketch(Plane.XY.offset(thickness)):
        with Locations((-slot_pitch / 2, base_depth - 12), (slot_pitch / 2, base_depth - 12)):
            SlotCenterToCenter(slot_length, slot_width, rotation=90)
    extrude(amount=-thickness, mode=Mode.SUBTRACT)

part = bracket.part
bb = part.bounding_box()
print(f"Size: {bb.size.X:.1f} x {bb.size.Y:.1f} x {bb.size.Z:.1f} mm")
print(f"Volume: {part.volume:.0f} mm^3, mass in 6061 aluminum: {part.volume * 2.70e-3:.0f} g")

export_step(part, "nema17_bracket.step")
export_stl(part, "nema17_bracket.stl")
```

## Common build123d mistakes

-   **The wrong plane direction.** `Plane.XZ` has its normal along negative Y, so a positive extrude on it goes toward negative Y. When a feature appears on the wrong side, check the `z_dir` of the plane, or define the plane yourself as Step 5 does.
-   **Selectors that match too much.** Print the length of a `ShapeList` before you fillet it. If it has more edges than you expect, add a filter or select earlier in the build.
-   **Fillets that fail.** A fillet radius larger than a face next to it makes the kernel fail. Fillet before you cut nearby holes, and keep each radius smaller than the faces it joins.
-   **Magic numbers.** A dimension typed into a feature call is hard to find later. Put it in a named parameter at the top of the script.

## Change the design without starting over

This is where code-based CAD pays off. To raise the shaft for a larger pulley, change `shaft_height`. The upright grows with it. For more belt travel, change `slot_length`. For a stiffer bracket, change `thickness` and run the script again. The fillets, holes, and slots follow, because each feature is placed from the parameters, not from fixed coordinates.

The bracket is still one part of a larger product, though. The motor needs a driver board. The board needs a connector, a mount, and an enclosure. When the belt load or the motor changes, the bracket, the board, and the housing all have to change together.

Flux designs all three in one project. Start with the bracket prompt above, then ask Flux for the driver board and the enclosure.

[Start Free Trial](https://www.flux.ai/signup)

## FAQs

### What is build123d?

build123d is an open-source Python library for parametric CAD, built on the OpenCascade kernel. You write parts as Python code and export exact solids as STEP, or meshes as STL.

### What is the difference between build123d and CadQuery?

Both use the OpenCascade kernel and make the same kind of solids. CadQuery chains methods on a workplane object. build123d uses Python `with` blocks in builder mode and operators in algebra mode, and its selectors filter, sort, and group edges and faces as Python lists.

### How do I install build123d?

Run `pip install build123d`. To see your part while you code, install the OCP CAD Viewer extension for VS Code and use `show()` from `ocp_vscode`.

### Can build123d export STEP files?

Yes. `export_step(part, "part.step")` writes an exact solid that other CAD tools and machine shops can open. `export_stl()` writes a mesh for 3D printing.

### Can Flux design parametric parts like this bracket?

Yes. Describe the part to Flux, and it writes editable CAD code with named parameters, builds the solid, and keeps the part editable. It can also import your STEP file, analyze stress and deflection, and export STEP, STL, drawings, and G-code. The board and the enclosure stay in the same project.

## About the author

[Nico Tzovanis](https://www.flux.ai/nico)

Nico is a professional electronics and PCB design engineer at Flux. Find him on Flux [@nico](https://www.flux.ai/nico)

Let’s start building

Build your big idea together with Flux. All you need to do is describe what you want to make, or what problem you want to solve. Flux will work with you to make it a reality.

![Close-up photo of a PCB made with Flux, with an ESP32 module, buttons, and a microSD slot.](https://www.flux.ai/p/assets/blogs/miscellaneous/sidebar-cta-pcb.webp) [Try it Free](https://www.flux.ai/signup)

## Related Content

[

![Flux, an end to end PCB design tool shown with a very complex linux card project](https://www.flux.ai/p/assets/blogs/8-principles-when-choosing-your-pcb-design-tool/main.webp)

### The 8 Principles of Choosing Cloud-Based Software for PCB Design

6 Min Read](https://www.flux.ai/p/blog/8-principles-of-choosing-cloud-based-software-for-pcb-design)[

![Teal isometric illustration of a robot arm joint with its exploded enclosure and motor controller board, connected to three cloud computers that show simulation results](https://www.flux.ai/p/assets/blogs/bring-your-own-software/main.webp)

### Bring Your Own Software to Flux

7 Min Read](https://www.flux.ai/p/blog/bring-your-own-software)[

![Teal isometric illustration of a PCB, motor, bearings, and fasteners beside a parts bin draining chips toward a distant data center server rack](https://www.flux.ai/p/assets/blogs/component-shortage-ai-boom-bom/main.webp)

### The AI Boom Is Eating Hardware's Parts. Fix Your BOM in Hours.

5 Min Read](https://www.flux.ai/p/blog/component-shortage-ai-boom-bom)[

![Close-up view of a pick-and-place machine in a PCB manufacturing facility, automating the process of placing components and circuit board printing.](https://www.flux.ai/p/assets/blogs/pcb-manufacturing/main.webp)

### Discovering PCB Manufacturing: Technologies That Shape the Industry

3 Min Read](https://www.flux.ai/p/blog/discovering-pcb-manufacturing-technologies-that-shape-the-industry)[

![Comparison image showing EasyEDA and Flux PCB design software interfaces with circuit board layouts. EasyEDA logo is on the left, and Flux logo is on the right. A 3d-rendered pcb board of a project that looks like made using both platforms.](https://www.flux.ai/p/assets/blogs/easyeda-vs-flux/main.webp)

### EasyEDA vs. Flux

6 Min Read](https://www.flux.ai/p/blog/easyeda-vs-flux)[

![Flux, an AI hardware engineer pointing out the dos and don'ts when prompting.](https://www.flux.ai/p/assets/blogs/prompting-guide/main.webp)

### Prompting Guide: Working with Flux from Idea to Schematic

9 Min Read](https://www.flux.ai/p/blog/prompting-guide-working-with-flux-from-idea-to-schematic)

See All (6)

## Let’s start building.

Let’s build your idea together. All you need to do is describe what you want to make, or what problem you want to solve. Flux will work with you to make it a reality.

[Try it Free](https://www.flux.ai/signup)

 ![Close-up of a Flux-designed printed circuit board](https://www.flux.ai/p/assets/homepage-v2/mission-visual.webp)
