build123d Tutorial: Model a NEMA 17 Motor Bracket in Python

build123d Tutorial: Model a NEMA 17 Motor Bracket in Python

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.

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

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
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.

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.

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.
    # 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.

    # 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.

    # 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.

    # 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.

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.

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

FAQs

About the author

Nico Tzovanis

Nico is a professional electronics and PCB design engineer at Flux. Find him on Flux @nico

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
Close-up of a Flux-designed printed circuit board