FreeCAD Backward Compatibility for Custom Scripts

Maintaining custom Python scripts across major FreeCAD releases requires handling underlying architectural changes, such as shifts in the core C++ bindings, GUI toolkit migrations, and Python API refactoring. This guide outlines practical strategies to keep your FreeCAD scripts backward compatible across major updates, focusing on dynamic version checking, API abstraction, Qt framework handling, and robust testing techniques.

Detect and Branch by FreeCAD Version

FreeCAD provides runtime version metadata through the base application object. Use this data to execute version-specific branches when an API changes significantly.

The FreeCAD.Version() function returns a list containing the major, minor, and build versions as strings:

import FreeCAD

major = int(FreeCAD.Version()[0])
minor = int(FreeCAD.Version()[1])

if (major, minor) < (0, 21):
    # Legacy API implementation
    pass
else:
    # Modern FreeCAD 0.21+ / 1.0+ implementation
    pass

Avoid hardcoding strict equality checks (minor == 20). Always use comparison tuples ((major, minor) >= (0, 21)) so your fallback logic functions properly on future point releases.

Handle the PySide2 to PySide6 Transition

One of the largest breaking changes between FreeCAD versions (notably moving toward FreeCAD 1.0) is the migration from Qt5 (PySide2) to Qt6 (PySide6).

To ensure your custom GUI dialogs and panels work across these environments, write agnostic Qt import wrappers using exception handling:

try:
    from PySide import QtGui, QtCore, QtWidgets
except ImportError:
    try:
        from PySide6 import QtGui, QtCore, QtWidgets
    except ImportError:
        from PySide2 import QtGui, QtCore, QtWidgets

Keep in mind that certain Qt modules and enum locations shifted in Qt6. For example, flags often require fully qualified enum namespaces in PySide6:

# Instead of relying on direct namespace access:
# alignment = QtCore.Qt.AlignLeft

# Use a safe helper or conditional assignment:
if hasattr(QtCore.Qt.AlignmentFlag, "AlignLeft"):
    ALIGN_LEFT = QtCore.Qt.AlignmentFlag.AlignLeft
else:
    ALIGN_LEFT = QtCore.Qt.AlignLeft

Build an API Abstraction Layer

When methods in core modules like Part, Draft, or Sketcher are renamed or their signatures change, avoid scattering if/else checks throughout your script. Instead, isolate these differences inside a dedicated compatibility module or wrapper class.

# compat.py
import FreeCAD

def create_circle(radius):
    """Abstracts differences in circle creation across workbench versions."""
    import Part
    if hasattr(Part, "makeCircle"):
        return Part.makeCircle(radius)
    else:
        # Fallback for alternative or legacy implementations
        import Draft
        return Draft.makeCircle(radius)

By centralizing version quirks in one file, maintaining and updating your codebase for future releases only requires updating the adapter layer.

Stick to the Documented Public API

Breaking changes often affect undocumented internals and private attributes. Minimize compatibility breakage by adhering to best practices:

  • Use standard property types: Define custom properties using canonical identifiers like App::PropertyLength, App::PropertyAngle, or App::PropertyFloat.
  • Rely on documented proxy methods: Implement standard feature methods (execute, onChanged, mustExecute) inside your custom Document Object proxies rather than hooking into transient C++ events.
  • Avoid monkey patching: Do not override core FreeCAD methods or inject unmanaged attributes directly into built-in objects.

Handle Headless Environments Defensively

Scripts that run in both the graphical interface and the background (such as automated export or rendering scripts) must gracefully handle the absence of the GUI:

import FreeCAD

# FreeCADGui may not be present in command-line mode
try:
    import FreeCADGui
    GUI_AVAILABLE = True
except ImportError:
    GUI_AVAILABLE = False

def update_view():
    if GUI_AVAILABLE:
        FreeCADGui.updateGui()

This prevents unexpected crashes when users switch execution modes between major versions.

Automate Multi-Version Testing

Code-level compatibility checks are only as reliable as your testing suite. Implement continuous integration that tests your script against multiple FreeCAD release environments using containerized or headless FreeCAD binaries.

Run test scripts via the command line:

freecadcmd -c "import test_suite; test_suite.run_all()"

Running tests across Docker images targeting different target releases (such as stable 0.20, 0.21, and the latest 1.0 release candidates) guarantees that deprecated calls are surfaced before breaking end-user workflows.