Pyzo is a free and open-source Python IDE focused on interactivity and introspection, which makes it well-suited for engineering and scientific applications. This page describes how to use it in conjunction with FreeCAD.

Unless otherwise noted, this page assumes the following software versions:
$ chmod +x ./FreeCAD_0.21.2-Linux-x86_64.AppImage$ ./FreeCAD_0.21.2-Linux-x86_64.AppImage --appimage-extract$ mv ./squashfs-root FreeCADexport FREECAD_USER_HOME=$HOME]#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
${HERE}/AppRun python "$@"
$ ./FreeCAD/AppRun and close it again so that the user config files are created properly.Note that, on some Linux systems, the FreeCAD 0.21.2 AppImage reports an error "MESA-LOADER: failed to open ..." when being started. In that case execute $ rm ./FreeCAD/usr/lib/libdrm* after extracting and renaming the AppImage, as suggested here.
python3 /path/to/pyzo-main/pyzolauncher.py. To use a specific Qt API instead of the automatically detected one, set environment variable "QT_API" before. Possible values are: pyside2, pyside6, pyqt5, pyqt6.python3 -m pip install pyzoLinux users please run Pyzo from source because there could be library incompatibilities with the provided binary version of Pyzo, resulting in a crash when opening the file dialog.
See also the chapter "Installation" in the Pyzo ReadMe: https://github.com/pyzo/pyzo#installation
Start Pyzo and enter the shell configuration dialog via the Menu:
Shell -> Edit shell configurations...
A dialog window will appear.
If there was no old shell configuration found, then Pyzo will create a single config named "Python" with an empty "exe" textbox. Fill out the fields with the values provided below.
Otherwise, if there is no config with an empty "exe" textbox, press the button "Add config" on the top right corner, and then fill out the fields:
from PySide2 import QtCore, QtWidgets # resp. PySide6 instead of PySide2
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts, True)
# optional switches:
# QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
# AFTER_GUI - code below runs after integrating the GUI
import os, sys
if sys.platform == 'linux': # ... and using the (extracted) AppImage
sys.path.append(os.environ['PATH_TO_FREECAD_LIBDIR'])
# some older Linux systems require the following line:
# os.environ['FREECAD_USER_HOME'] = os.environ['HOME']
sys._stdout_pyzo = sys.stdout
import FreeCAD as App
import FreeCADGui as Gui
App.ParamGet('User parameter:BaseApp/Preferences/OutputWindow').SetBool('RedirectPythonOutput', False)
App.ParamGet('User parameter:BaseApp/Preferences/OutputWindow').SetBool('RedirectPythonErrors', False)
Gui.showMainWindow()
sys.stdout = sys._stdout_pyzo
PYZO_PROCESS_EVENTS_WHILE_DEBUGGING=1
LC_NUMERIC=C
The PySide version (2 or 6) in the fields "gui" and "startupScript" depends on the Qt library version used by FreeCAD: Qt 5.x needs PySide2, and Qt 6.x needs PySide6. The Qt version can be seen in FreeCAD's "About" dialog.
Finally, press button "Done" in the shell configuration dialog. Run a new "freecad" shell via Pyzo's "Shell" menu. This will start the FreeCAD Gui and a will open a FreeCAD Python shell in Pyzo.
On Linux, when not using an (extracted) AppImage, change sys.path.append(os.environ['PATH_TO_FREECAD_LIBDIR']) in the text field "startupScript" to e.g. sys.path.append('/builds/freecad-source/build/lib'), specifying the directory to the library files "FreeCAD.so" and "FreeCADGui.so". This library directory path could also be added to the field "pythonPath" in the Pyzo shell configuration dialog instead.
The environment variable entry PYZO_PROCESS_EVENTS_WHILE_DEBUGGING=1 will tell Pyzo to periodically update FreeCAD's Qt-GUI while FreeCAD is stopped during a breakpoint.
Do not remove the # AFTER_GUI comment in the newly entered startup code -- this is a separator used by Pyzo to split the code into two blocks.
If you have an older Mac and pyzo fails to launch (e.g. pyzo 4.13.2 on OS-X 10.15), a possible work-around is to launch a patched pyzo 4.12.7 from the Terminal. See FreeCAD forum message
See https://pyzo.org/guide.html and run the Pyzo Wizard (Menu: Help -> Pyzo Wizard) to get a short introduction.
Noteworthy information:
pyzo.config.shellConfigs2.insert(0, pyzo.config.shellConfigs2[1].copy()).__file__ variable defined in the interactive mode, but as a workaround you can run import inspect; __file__ = inspect.getfile(inspect.currentframe()) to get it.A normal workflow to automate FreeCAD operations is similar to using just FreeCAD. The shell in Pyzo has access to the same Python interpreter as the "Python console" panel in the FreeCAD GUI. Both can be used in a mixed fashion, whatever is more convenient in the situation.
Pyzo brings very nice features to this workflow, just to list some of them:
Have a look here and here to see this in action (with an older version where the FreeCAD start was not yet included in the Pyzo shell configuration).
inner_cylinder = Part.makeCylinder(innerRadius, height).outer_cylinder.rotate((0, 0, 0), (0, 1, 0), 45) in Pyzo's shell.inner_cylinder = Part.makeCylinder(innerRadius, height).return shape.This is a step-by-step demonstration of how breakpoints can be used to debug callbacks in FeaturePython objects.
import sys
sys.path.append('/tmp/mymodules')
import mytest
doc = App.newDocument()
mytest.create('abcde')
obj.Shape = ... in method execute(self, obj) of class Box.pass in Pyzo's shell or just press Return there so that the breakpoint will be applied.obj.Height in the shell. Change the value by executing obj.Height = 400 and continue execution via "Shell -> Debug continue". Switch back to the FreeCAD GUI -- the box shape now has a height of 400 mm.Normally, the code for FeaturePython objects is part of a module. Once FreeCAD objects are created, changes to the code in the module will only take effect after closing the FCStd-file, reloading the module, and re-opening the FCStd file.
To make quick iteration cycles during development of FeaturePython code, monkey patching strategies can be used, as demonstrated with the following example.
First we create the module with the FeaturePython object's code. Create a new python script with the following code and save it into your user macros directory as "mymod.py".
import FreeCAD as App
def create(obj_name):
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)
MyBox(obj)
App.ActiveDocument.recompute()
return obj
class MyBox():
def __init__(self, obj):
self.Type = self.__class__.__name__
obj.Proxy = self
def execute(self, obj):
# called on document recompute
self.my_computations(obj)
## start of code for monkeypatching
def my_computations(self, obj):
print(obj.Label, 'aaa')
if '__module__' not in dir():
def _monkeypatch_method(classname, method):
import inspect, os, sys
__this_file__ = os.path.abspath(inspect.getfile(inspect.currentframe()))
for mod in sys.modules.values():
if __this_file__.startswith(getattr(mod, '__file__', None) or '_'):
setattr(getattr(mod, classname), method.__name__, method)
_monkeypatch_method('MyBox', my_computations)
## end of code
We assume that, at this point, your already have started FreeCAD via Pyzo. Execute the following code (either in Pyzo's FreeCAD-shell or in FreeCAD's Python console panel):
import mymod
doc = App.newDocument()
mymod.create('obj1')
mymod.create('obj2')
doc.recompute()
Now, everytime you recompute one of the two created FeaturePython objects, the object's label and "aaa" will be printed in Pyzo's shell.
We want to quickly change the behavior of the FeaturePython object so that it prints "bbb" instead of "aaa". Open the module in Pyzo's editor (if not already opened) and change the string literal "aaa" to "bbb". To apply these changes, just execute that cell (which is defined by the two "##" comment lines) in Pyzo by pressing Ctrl+Return. Recompute the objects, and we have the desired result. Editing the method and executing the cell can now be repeated as often as desired. Debugging also works normally, e.g. using breakpoints. If you want set breakpoints below the code cell, there might be a line offset because the rest of the class definition is not updated. A nice aspect of this strategy is that the code is directly modified in the real module, so when just saving the module, restarting FreeCAD and re-opening the FCStd file, you already have the newest version without any monkey patching.

Pyzo includes a Plug-In concept to extend its features without patching its source code.
See the example code in the Pyzo repository:
https://github.com/pyzo/pyzo/blob/main/pyzo/resources/code_examples/myRunner.py
Init scripts of a FreeCAD workbench, e.g. files such as Mod/Draft/Init.py or Mod/Draft/InitGui.py can be debugged using Pyzo like any other Python script, for example by placing breakpoints in these files.
But in older FreeCAD versions < v1.0, such as v0.21.2, we need to apply a patch so that the Python interpreter knows the path of the executed init script. So, for older FreeCAD versions, follow the instructions in chapter Extracting Python code used for initializing FreeCAD modules.
We can now, for example, open Mod/Draft/Init.py in Pyzo, set a breakpoint there in line 28 and start the FreeCAD shell in Pyzo. Execution will be interrupted at the breakpoint. We can switch the stack frames, view and manipulate variables and step/continue the code execution. To see and debug Python code from higher levels, i.e. from callers in the FreeCAD library, the corresponding Python code must be extracted before, as described in the following chapter.
During startup, FreeCAD executes the Python scripts src/App/FreeCADInit.py and src/Gui/FreeCADGuiInit.py as a string inside a compiled C++ library, and each of these scripts reads the corresponding init scripts (App and Gui) of the workbench and executes them using the Python exec command.
The scripts src/App/FreeCADInit.py and src/Gui/FreeCADGuiInit.py are not individual files anymore but resource files in libraries in the distributed binary versions of FreeCAD. This makes it difficult to modify these scripts, and this makes it also difficult to debug that Python code.
But there is a work-around: The init scripts can be extracted to normal files which are then executed by the FreeCAD library. The extracted scripts can then be opened in Pyzo, modified and debugged like any other Python script.
The following code will extract the scripts from the libraries, and place the caller code back in the libraries. The start-up code for older FreeCAD versions < v1.0 will also be patched, as mentioned in the previous chapter.
import os
import sys
import shutil
lib_dirpath = '/home/.../FreeCAD/usr/lib' # example for linux os
# lib_dirpath = r'C:\ProgramData\FreeCAD\bin' # example for windows os
restore_original_libs = False
# restore_original_libs = True
lib_dirpath = os.path.abspath(lib_dirpath)
for appgui in ['App', 'Gui']:
if sys.platform == 'linux':
fp_lib = os.path.join(lib_dirpath, 'libFreeCAD{}.so'.format(appgui))
elif sys.platform == 'win32':
fp_lib = os.path.join(lib_dirpath, 'FreeCAD{}.dll'.format(appgui))
else: raise NotImplementedError()
fp_backup = fp_lib + '.orig'
if restore_original_libs:
shutil.copy(fp_backup, fp_lib)
print('restored library file:', fp_lib)
continue
if not os.path.isfile(fp_backup):
shutil.copy(fp_lib, fp_backup)
print('created library backup file:', fp_backup)
with open(fp_backup, 'rb') as fd:
dd = fd.read()
if appgui == 'App':
needle_cands = [b'\n# FreeCAD init module\n', b'\n# FreeCAD init module - App\n']
fp_py = os.path.abspath(os.path.join(lib_dirpath, '..', 'FreeCADInit.py'))
else:
needle_cands = [b'\n# FreeCAD gui init module\n', b'\n# FreeCAD init module - Gui\n']
fp_py = os.path.abspath(os.path.join(lib_dirpath, '..', 'FreeCADGuiInit.py'))
for needle in needle_cands:
# try different needles because of differences in FreeCAD versions
i_needle = dd.find(needle)
only_one_result = dd.find(needle, i_needle + 1) == -1
if i_needle != -1 and only_one_result:
break # found
else:
raise ValueError('needle not found')
i_start = i_needle - dd[:i_needle][::-1].index(b'\x00')
i_end = dd.index(b'\x00', i_needle)
dd_code = dd[i_start:i_end]
loader_code = '\n'.join([
'fp_lib = ' + repr(fp_py),
'with open(fp_lib, "rt", encoding="utf-8") as fd: source = fd.read()',
'exec(compile(source, fp_lib, "exec"))',
]).format(repr(fp_py))
dd_code_new = loader_code.encode('utf-8')
assert len(dd_code_new) <= len(dd_code)
dd_code_new += b'\x00' * (len(dd_code) - len(dd_code_new))
dd_new = dd[:i_start] + dd_code_new + dd[i_end:]
assert len(dd_new) == len(dd)
with open(fp_lib, 'wb') as fd:
fd.write(dd_new)
print('modified library file:', fp_lib)
tt_code = dd_code.decode('utf-8')
# patch init code of older FreeCAD versions < v1.0
# see https://github.com/FreeCAD/FreeCAD/pull/10618
needle = """
with open(file=InstallFile, encoding="utf-8") as f:
exec(f.read())
"""
needle_patched = """
with open(InstallFile, 'rt', encoding='utf-8') as f:
exec(compile(f.read(), InstallFile, 'exec'))
"""
if needle in tt_code:
tt_code = tt_code.replace(needle, needle_patched)
print('applied patch to code:', fp_py)
with open(fp_py, 'wt', encoding='utf-8') as fd:
fd.write(tt_code)
print('extracted init code (that will be called from the lib) to file:', fp_py)
https://forum.freecad.org/viewtopic.php?p=720709#p720709
A modified "workspace" tool that adds filters and a watchlist.
https://forum.freecad.org/viewtopic.php?p=774260#p774260
A modified "workspace" tool that adds an extra treeview widget for displaying "watched" variables.
https://forum.freecad.org/viewtopic.php?p=774810#p774810
Displays a push button to open a git bash shell in the path of the file in the current editor.
To ask questions about this topic, share ideas, give input, point out mistakes, etc, please write a message to the initial topic in the FreeCAD forum or create a new one.