?>

How 3D Models Are Generated Using build123d

Discover how 3D files are modeled with build123d under Python, the open-source parametric modeling library used on iteration3d.

build123d as the core engine

On iteration3d, the main 3D file generation engine is based on the tool build123d.

It is an open-source Python program for parametric modeling under the Apache 2.0 License. This framework is built on the Open Cascade geometry kernel (OCCT - maintained by Capgemini). The popular open-source modeling software FreeCAD uses this same core. build123d can be considered an evolution of CadQuery.

What is code-driven parametric modeling?

When using tools like Fusion 360 or SolidWorks, you're used to drawing a sketch with a mouse or stylus and then extruding it to get a 3D shape. Script-based parametric modeling works differently — instead of “drawing in 3D,” you actually program in 3D. Fusion 360 and SolidWorks are indeed parametric modelers under the hood: you can change a dimension or adjust an extrusion value at any time. On top of that, they provide a graphical interface that makes the whole process much easier for the user.

Let’s get back to our topic: parametric modeling through code. OpenSCAD is a program based on this principle and serves as an accessible entry point to parametric modeling. To put it simply, each "shape" is declared programmatically. In OpenSCAD, the instruction cube([10, 10, 10]) will generate a cube (as you’ve guessed) of 10x10x10. Easy, right? Create another cube, move it with another command, and gradually your program creates a new 3D object.

build123d follows the same principle, in a more complete and therefore more complex way — or maybe not. The build123d documentation even includes a transition tutorial from OpenSCAD.

The power of Python in build123d

The main strength of build123d lies in its full use of the powerful Python programming language. We'll stop here, as the goal of this page is educational, but developers will see the potential of this coding language when applied to parametric modeling.

As an example, the following build123d code will produce a rectangular prism measuring 100 x 42 x 57 mm (Model #768):


from build123d import *

with BuildPart() as thisObject:
    Box(100, 42, 57)
            
rectangular prism build123d 100x42x57
Build123d rectangular prism example

Of course, there’s a whole set of instructions to move, merge, subtract, and more. This allows you to generate very complex 3D shapes — and more importantly, to do so in a parametric way, meaning the model is easily modifiable by simply adjusting the variables you've defined.

How to Get Started with build123d?

To start modeling with build123d, you will need to have some simple but essential knowledge of Python programming.

Build123d provides detailed documentation.

The best starting point is probably this simple and well-structured tutorial on designing a 3D part with Build123d. Then, many additional examples are available.

Build123d has a very responsive Discord channel, with regular contributions from the project’s developers.

To greatly facilitate debugging, it is very useful to install the graphical interface OCP CAD Viewer for VS Code, which integrates very easily and directly into Visual Studio Code.

You can find the official build123d GitHub repository here.

And to conclude: thank you build123d, thank you CadQuery, thank you OCCT, thank you Python!

Complete build123d Tutorial

For this tutorial, we’ll use the 3D Bowl / Pot template as our reference. As noted above, we use Visual Studio Code as the source code editor, and OCP CAD Viewer for VS Code to visualize on-screen the sketches and models we produce. The screenshots shown in this tutorial come from OCP CAD Viewer for VS Code.

Important: the code proposed here isn’t the best, and the naming isn’t ideal. There are other ways to obtain the same model faster and more efficiently. The purpose here isn’t to "code well" but to provide a tutorial that helps you grasp the fundamentals of build123d and illustrates many construction functions from the build123d library.

Define the parameters and import the tools

Let’s assume we want to define our bowl with these parameters:

This is a decisive step that will determine the entire code construction. You can consult On-demand 3D file generation: the process to learn more about this first step, which is part of an overall workflow.

We also want the base of the bowl rounded at the corner, so we introduce a new variable bend_fillet and set it to 1.

That gives us these initial lines of code:


from build123d import *
from ocp_vscode import show

from math import atan2, tan, pi

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2

bend_fillet       = 1.0
    

From these first lines, we’re aiming for a bowl with a wall thickness of 2, an inner height of 70, an inner bottom diameter of 50, and an inner top diameter of 80.

You’ll notice several lines starting with import. These import the Python libraries the script needs:

Draw a path

We want to generate a bowl. A bowl is a symmetrical shape around a central axis: it can be created by revolving a 2D profile, whose rotation produces a perfectly regular geometry around that axis. So first, we’ll generate a path that will later serve as the basis for the revolution.

This path will be defined by three points. The first is at the origin (0.0, 0.0) and the second lies along the X-axis, at a distance of inner_bottom_diam from the origin (inner_bottom_diam / 2.0, 0.0). We divide by two to get the radius from the diameter.

For the third point (the top of the bowl), things are a bit more complex. We’ve decided that the bowl’s rim will be parallel to the base (parallel to the X-axis), not tilted. That’s tricky to picture and explain at this stage, so we’ll intentionally place the third point at twice the intended height—in other words, twice the inner height, inner_height. To be able to “stretch” this segment, we’ll compute the angle implied by the bowl’s dimensions. Then, depending on whether that angle is vertical or not, we’ll apply specific handling.

This yields the following code:


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

    

Now that we have the three points, we can build the path. To do this, we use FilletPolyline, which creates a path with a fillet (a rounded corner) so the base of our bowl is softened. FilletPolyline is the first construction function we use in our script. show lets us visualize the path, in purple on screen.


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

# 1) Filleted polyline (wire path)
with BuildLine() as ln:
    FilletPolyline((x0, y0), (x1, y1), (x2, y2), radius=bend_fillet)
path = ln.wire()

show(path)

            
view of the path generated by build123d
View of the path in purple

Extrude a 3D shape

To revolve around the Y-axis, we need a 2D profile—a closed shape, a face. Our path is only a segment. So we’ll “extrude” a square along this path to obtain a kind of bent 3D bar.

To do this, we use the Rectangle and sweep construction functions. Rectangle defines a 2D rectangle (here a square, since length equals width) with side thickness, the desired wall thickness of our bowl. sweep “sweeps” the square profile along the path to create a 3D shape—a bent bar shown in transparency thanks to transparent=True.


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

# 1) Filleted polyline (wire path)
with BuildLine() as ln:
    FilletPolyline((x0, y0), (x1, y1), (x2, y2), radius=bend_fillet)
path = ln.wire()



# 2) Sweep a square section along that path
with BuildPart() as swept:
    with BuildSketch(path ^ 0):
        Rectangle(thickness, thickness, align=(Align.MAX, Align.MIN))
    sweep(path=path, transition=Transition.RIGHT)

show(path, swept, transparent=True)
            
view of the 3D shape generated by a sweep in build123d
Transparent view of the 3D shape (a bent bar), obtained by “sweeping” the 2D square along the purple path
zoomed-in bottom view of the purple path used for the sweep and the resulting 3D shape
Zoomed, bottom view of the purple path, the support for the sweep and generated 3D shape

Trim the 3D shape

Recall that we “stretched” the inclined arm. Now we need to trim it. We set the cut height to thickness + inner_height—that is, wall thickness plus inner height. This will be the bowl’s total height.

If you look closely at the 3D shape at this stage, you’ll see it isn’t aligned along X but shifted by thickness below the X-axis. The simplest approach would be to move the part toward +Y by thickness. But we’ll do it differently to introduce how to locate a specific face.

If we want to cut at thickness + inner_height, we need to position ourselves under the 3D shape. Here we use filter_by(Plane.XZ) to select only the faces parallel to the XZ plane of the swept part. Then we sort them by their position along the Y-axis with sort_by(Axis.Y) to get the lowest face, which will serve as our reference.


...

# 3) Cut the angled arm using a plane parallel to XZ
target_y = thickness + inner_height

# Pick the lowest XZ-parallel face as a reference
xz_faces = swept.part.faces().filter_by(Plane.XZ).sort_by(Axis.Y)

ref_face = xz_faces[0]  # EN: lowest-Y face

# Build a reference plane from that face
ref_plane = Plane(ref_face)

show(path,swept.part,ref_plane)

...

            
a small new marker appears on the 3D model, representing our new reference plane
You can see a new small triad under the 3D shape as intended: that’s our new reference plane

Now we want to ensure that the reference plane’s normal points upward, that is, in the +Y direction, like the standard Plane.XZ. If the normal is oriented the other way (toward −Y), we invert it. Then we compute the offset distance between the plane’s current position and the target height Y = thickness + inner_height. This lets us create a new offset plane (cut_plane) at exactly the right height to perform the cut. We then trim our 3D shape with split.


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

# 1) Filleted polyline (wire path)
with BuildLine() as ln:
    FilletPolyline((x0, y0), (x1, y1), (x2, y2), radius=bend_fillet)
path = ln.wire()



# 2) Sweep a square section along that path
with BuildPart() as swept:
    with BuildSketch(path ^ 0):
        Rectangle(thickness, thickness, align=(Align.MAX, Align.MIN))
    sweep(path=path, transition=Transition.RIGHT)



# 3) Cut the angled arm using a plane parallel to XZ
target_y = thickness + inner_height

# Pick the lowest XZ-parallel face as a reference
xz_faces = swept.part.faces().filter_by(Plane.XZ).sort_by(Axis.Y)

ref_face = xz_faces[0]  # EN: lowest-Y face

# Build a reference plane from that face
ref_plane = Plane(ref_face)

show(path,swept.part,ref_plane)

# Ensure the plane's normal points to +Y (same convention as Plane.XZ) so Keep.TOP keeps the upper side
if ref_plane.z_dir.dot(Vector(0, 1, 0)) < 0:
    ref_plane = Plane(ref_plane.origin, ref_plane.x_dir, -ref_plane.z_dir)

# Compute the signed offset to reach target Y (thickness + inner_height)
offset_dist = target_y - ref_plane.origin.Y
cut_plane = ref_plane.offset(offset_dist)



with BuildPart() as trimmed:
    add(swept.part)
    split(bisect_by=cut_plane, keep=Keep.BOTTOM)

final_part = trimmed.part

show(path,final_part)
            
our 3D shape is now properly cut and no longer follows the entire path
Our 3D shape is properly trimmed and no longer follows the entire path

Prepare the revolve face

Remember, the goal of building this bent 3D bar is to get a profile we can rotate around the Y-axis. But you can’t revolve a solid directly. So we’ll extract the bar’s face that lies on the XY plane.


...
# 4) Get the seed_face (face lying on the XY plane, i.e., Z ≈ 0)
xy_faces = final_part.faces().filter_by(Plane.XY)

# Pick the XY face whose plane is closest to Z = 0
seed_face = min(xy_faces, key=lambda f: abs(Plane(f).origin.Z))

show(path,seed_face)
...
            
we have obtained the 2D face that allows us to perform a revolution around the Y axis
We’ve obtained the (2D) face that will let us apply a revolution around the Y-axis

Revolution (revolve)

Here we are! We can now apply the revolution with revolve.


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

# 1) Filleted polyline (wire path)
with BuildLine() as ln:
    FilletPolyline((x0, y0), (x1, y1), (x2, y2), radius=bend_fillet)
path = ln.wire()



# 2) Sweep a square section along that path
with BuildPart() as swept:
    with BuildSketch(path ^ 0):
        Rectangle(thickness, thickness, align=(Align.MAX, Align.MIN))
    sweep(path=path, transition=Transition.RIGHT)



# 3) Cut the angled arm using a plane parallel to XZ
target_y = thickness + inner_height

# Pick the lowest XZ-parallel face as a reference
xz_faces = swept.part.faces().filter_by(Plane.XZ).sort_by(Axis.Y)

ref_face = xz_faces[0]  # EN: lowest-Y face

# Build a reference plane from that face
ref_plane = Plane(ref_face)

# Ensure the plane's normal points to +Y (same convention as Plane.XZ) so Keep.TOP keeps the upper side
if ref_plane.z_dir.dot(Vector(0, 1, 0)) < 0:
    ref_plane = Plane(ref_plane.origin, ref_plane.x_dir, -ref_plane.z_dir)

# Compute the signed offset to reach target Y (thickness + inner_height)
offset_dist = target_y - ref_plane.origin.Y
cut_plane = ref_plane.offset(offset_dist)



with BuildPart() as trimmed:
    add(swept.part)
    split(bisect_by=cut_plane, keep=Keep.BOTTOM)

final_part = trimmed.part


# 4) Get the seed_face (face lying on the XY plane, i.e., Z ≈ 0)
xy_faces = final_part.faces().filter_by(Plane.XY)

# Pick the XY face whose plane is closest to Z = 0
seed_face = min(xy_faces, key=lambda f: abs(Plane(f).origin.Z))


# 5) Revolve the seed_face into a solid bowl
with BuildPart() as revolved:
    revolve(seed_face, axis=Axis.Y)
final_part = revolved.part

show(path,final_part)

            
the revolution allows us to generate our 3D bowl
The revolution gives us our 3D bowl

Apply fillets

The rim edges are still sharp. We’ll apply a fillet—for aesthetics, but also to address the sometimes confusing fillet selection problem. Unlike Fusion 360, where a simple click on the edge suffices, here we must locate those edges programmatically. In our example this is fairly simple, but for complex models it can be much more tedious.

We’ll filter all the solid’s edges to keep only those of circular type (GeomType.CIRCLE), then sort them by their position along the Y-axis to isolate the two highest edges that form the bowl’s rim.


...
# 6) Select the two highest circular edges in Y
edges_sorted = (
    final_part.edges()
    .filter_by(GeomType.CIRCLE)               # keep only circular edges
    .sort_by(Axis.Y)                          # sort by Y position
)
target_edges = edges_sorted[-2:]              # take the two highest

show(target_edges)
...
            
we can slightly distinguish the two circular edges at the top of the bowl.
The two circular rim edges are faintly visible in purple at the top of the bowl

All that’s left is to add the fillets with fillet, which gives us the final code below:


from build123d import *
from ocp_vscode import show

from math import atan2, tan

# --- Inputs ---
inner_bottom_diam = 50
inner_top_diam    = 80
inner_height      = 70
thickness         = 2


bend_fillet       = 1.0


x0, y0 = 0.0, 0.0
x1, y1 = inner_bottom_diam / 2.0, 0.0

dx0 = (inner_top_diam - inner_bottom_diam) / 2.0
dy0 = inner_height
angle_rad = atan2(dy0, dx0)

y2 = inner_height * 2.0
if abs(dx0) < 1e-9: # vertical wall case (top==bottom)
    x2 = x1
else:
    x2 = x1 + y2 / tan(angle_rad)

# 1) Filleted polyline (wire path)
with BuildLine() as ln:
    FilletPolyline((x0, y0), (x1, y1), (x2, y2), radius=bend_fillet)
path = ln.wire()



# 2) Sweep a square section along that path
with BuildPart() as swept:
    with BuildSketch(path ^ 0):
        Rectangle(thickness, thickness, align=(Align.MAX, Align.MIN))
    sweep(path=path, transition=Transition.RIGHT)



# 3) Cut the angled arm using a plane parallel to XZ
target_y = thickness + inner_height

# Pick the lowest XZ-parallel face as a reference
xz_faces = swept.part.faces().filter_by(Plane.XZ).sort_by(Axis.Y)

ref_face = xz_faces[0]  # EN: lowest-Y face

# Build a reference plane from that face
ref_plane = Plane(ref_face)

# Ensure the plane's normal points to +Y (same convention as Plane.XZ) so Keep.TOP keeps the upper side
if ref_plane.z_dir.dot(Vector(0, 1, 0)) < 0:
    ref_plane = Plane(ref_plane.origin, ref_plane.x_dir, -ref_plane.z_dir)

# Compute the signed offset to reach target Y (thickness + inner_height)
offset_dist = target_y - ref_plane.origin.Y
cut_plane = ref_plane.offset(offset_dist)



with BuildPart() as trimmed:
    add(swept.part)
    split(bisect_by=cut_plane, keep=Keep.BOTTOM)

final_part = trimmed.part


# 4) Get the seed_face (face lying on the XY plane, i.e., Z ≈ 0)
xy_faces = final_part.faces().filter_by(Plane.XY)

# Pick the XY face whose plane is closest to Z = 0
seed_face = min(xy_faces, key=lambda f: abs(Plane(f).origin.Z))


# 5) Revolve the seed_face into a solid bowl
with BuildPart() as revolved:
    revolve(seed_face, axis=Axis.Y)
final_part = revolved.part


# 6) Select the two highest circular edges in Y
with BuildPart() as fp:
    add(final_part)
    edges_sorted = (
        fp.part.edges()
        .filter_by(GeomType.CIRCLE)
        .sort_by(Axis.Y)
    )
    target_edges = edges_sorted[-2:]
    fillet(target_edges, 0.6)

show(fp)
            
view without fillet applied
View without fillet
view with fillet applied
View with fillet

Final object

And that’s it—our bowl is modeled!

As mentioned at the outset, this isn’t the simplest or fastest way to obtain this 3D volume.

The idea was to explore as many construction functions as possible in this context and to give an overview of the build123d workflow. There are many other construction functions we didn’t use here. We also didn’t cover algebra mode.

Time to explore further and make the most of this very powerful library!

view of the 3D modeled bowl.
View of the modeled 3D bowl