Panther offers multiple ways to incorporate Python into your application:
appinit.py, a startup script, executed at application launch, placed in SMPATH or within a Panther library.Panther also provides built-in Python classes that mirror core application objects (App, Screen, Field, etc.). Additionally, Python functions can be used as entry, exit, and validation functions, as well as in control strings and JPL.
Python support is enabled by default in Panther 5.60 for Panther Client. Python is not yet supported for Panther Web Application Broker and Panther multithreaded servers, such as the Panther Application Servers for COM, JakartaEE (EJBs), JCO, JetNet and Tuxedo.
Where supported, the interpreter is dynamically loaded using the SMPYTHONLIBRARY environment variable, which should point to the shared library for a supported Python 3.x interpreter (3.9 or later). The variable assignment can be made within the Environment section of the INI file, or externally within the OS for Panther Client.
Panther loads and unloads the interpreter via calls to sm_py_init() and sm_py_finalize(), included in the provided jmain.c, jxmain.c. These calls are conditionally compiled based on the SM_PYTHON macro, which is defined by default in the end-user makefile that is provided in the link folder.
Python support can be disabled by commenting out lines in the Panther Python Parameters section of the end-user makefile and rebuilding the affected executables. For 64-bit Panther for Windows, for example, comment out the lines:
PYTHONCFLAGS = -DSM_PYTHON=1 PYLIB = "$(SMBASE)\lib\libpyw64.lib"
Regardless of whether Python support is compiled and linked in, if SMPYTHONLIBRARY is not defined at runtime, the interpreter will not be loaded—this is handled silently with no error or warning, and the application runs without executing any Python code that may be assigned to screens.
In the Screen Editor, each screen has a Python Script property under the FOCUS section. Selecting this opens a non-modal editor to input Python code associated with the screen. The Editor button launches an external editor, which may be specified using the SMPYEDITOR environment variable. If SMPYEDITOR is not provided, the value for SMEDITOR will be used instead, the same as for JPL. In Windows, SMPYEDITOR may also be set in the Environment section of the INI file.
Note that SMPYEDITOR is not used when double-clicking on a Python source file within the Library Table of Contents screen. In that case, the Editor will open whatever application is associated with the .py filename extension.
The following criteria apply to the Python Script assigned to a screen:
globals(), includes a self variable, Panther constants, and all prototyped functions, including Panther library functions - self is a reference to the Screen instance, and may be used to reference and modify properties of the screen.customer_id.value = "12345") if the names are valid Python identifiers._app, which is a singleton instance of Panther’s Appclass.The editor performs a basic syntax check during save, but many Python errors can only be detected at runtime. If there is a compilation error, the built-in editor repositions the caret cursor to the start of the line containing the error. It also shows the current line number on the status bar when editing.
If the interpreter isn't available at design time, the editor allows saving Python code anyway—without performing syntax validation.
When embedding Python code within a Panther screen, the script is saved using the single-byte-per-character encoding active during the editing session—just like JPL code and initial field text.
However, the Python interpreter expects source code to be UTF-8 encoded. To bridge this gap, Panther automatically converts the script to UTF-8 when:
⚠️ Important for Internationalization: If your script includes characters outside the standard ASCII range (code points > 127), you should escape them within string literals using Unicode escape sequences (e.g., \u00E9 for é). This ensures the script remains portable across different encodings and platforms.
Alternatively, you can save the full Python code in a separate UTF-8 encoded file and embed a single line in the Panther screen to import it:
import my_script
To use Visual Studio Code as the external editor in Windows, for example, set SMPYEDITOR or SMEDITOR to:
<path-to-VS Code>\Code.exe --wait %s
or
<path-to-VS Code>\Code.exe --new-window --wait %s
The first option will use an open VS Code IDE or open a new one if necessary. The second option uses the --new-window flag to force a new VS Code IDE to always be opened. In both cases, a new tab will be created in VS Code for editing a Python temp file provided by Prodev. Any changes made must be saved to that temp file, in order for Prodev to receive them. The --wait flag is always required. Access to Prodev is blocked when this flag is used, enabling Prodev to read back in the temp file only when you are done working with it externally.
Closing only the VS Code tab containing the temp file is sometimes insufficient for Prodev to continue. Closing all tabs and the IDE may be necessary, depending upon how VS Code happens to be managing its open tabs, files, and processes. If you find that you are having to close existing tabs for other work while using the --wait flag alone, consider using it with the --new-window flag, as shown.
Visual Studio Code provides other command-line options that can be used. Refer to Visual Studio Code documentation for further information.
An optional application-provided initialization script named appinit.py is compiled and exec’d by Panther’s Python framework within an instance method of the Appinstance. This occurs at the end of the initialization performed by the C function, sm_py_init(). The call to this function comes before any screen is opened, and is located in C source code that is provided for application customization (e.g., jxmain.c, jmain.c).
appinit.py may be used for:
debugpy)._app.title += f" (Version: {version})")Within this script, self is a reference to the singleton instance of the Appclass (a.k.a., _app). As such, Python variables defined within appinit.py are not externally accessible as Python attributes of _app using dot-syntax, since dot-syntax is used almost exclusively by Panther’s proxy classes for Panther Properties. Since appinit.py is not imported when Panther executes its code, one cannot get at its variables from a screen’s Python Script by using ‘import appinit’, as this will re-execute the script’s top-level code. One can, however, import another custom module from within appinit.py, in which application state variables are defined. A screen’s Python Script can then import that module to gain access to its state variables.
Alternatively, variables defined in appinit.py can be accessed by way of the execution context dictionary that Panther assigns as the _exec_context attribute of _app.
appinit.py:myvar = "Hello, World"context = _app._exec_context
print(context["myvar"])  # prints "Hello, World"
Note: Imported modules and their variables persist, except when toggling between Test Mode and Edit Mode in Prodev. Therefore, the values assigned to those variables persist, even when those module names and variable names are out of scope.
An application need not provide appinit.py if no Python start-up code is desired. Panther provides a default implementation for this file that imports smprintbx.py, but does nothing more.
Python functions within a screen and appinit.py are automatically registered for foreign function invocation by Panther, provided they meet specific criteria:
int or None are registered automatically.str or int are not recognized.appinit.py are global and persist for the lifetime of the Panther process.Python functions that are registered for Panther invocation behave like C hook functions that are installed as Prototyped Functions:
When the same function name is implemented in different technologies, Panther only ever calls one of them, the one that has the highest precedence.
While Python hook functions receive a self parameter referencing the screen, whether defined as a parameter or not, they are true Python functions—not methods. For other arguments, types are mapped as needed between technologies.
It is best to catch and handle exceptions in your Python code. If you allow an exception to propagate out of the Python screen code when it is first executed, its functions will not be available as Panther hook functions. This is true even if a portion of your code that defines them has been executed prior to the exception.
Python functions are registered by Panther in the same manner as C Prototyped functions, according to a mapping of Python type hints to Panther prototypes. In the table that follows, the prototype for a particular Parameter Type Hint is either an s or an i among, potentially, others in a coma-separated list for multiple parameters. For example, Panther's internal Prototyped Function List for its C functions contains the following entry for sm_i_fptr:
SM_STRFNC ("sm_i_fptr(s,i)", sm_i_fptr),
This is the same format as one uses to specify custom C Prototyped Functions in funclist.c.
If it were a Python function, it would be defined with type hints like this:
def sm_i_fptr(field_name:str, occurrence:int)->str:
Whether called from JPL or otherwise, Panther calls Python hook functions from C internally.  In doing so, a char* is conveted to a str and an int to an int.  For return values, a str is converted to a char*, an int to an int, a double to a float, and None to a 0.  Values that don't match the type hint typically result in a TypeError being raised, but may be converted to the required type if reasonable.
| Parameter Type Hint | Parameter Prototype | Python Conversion | Notes | 
|---|---|---|---|
| :str | s | str(arg) | |
| :int | i | int(arg) | |
| missing hint | - | - | not registered | 
| other than :int or :str | - | - | not registered | 
| Return Type Hint | Python Conversion | Return Prototype | Notes | 
|---|---|---|---|
| ->str | str(arg) | SM_STRFNC | |
| ->int | int(arg) | SM_INTFNC | |
| ->float | float(arg) | SM_DBLFNC | |
| ->None | None | SM_ZROFNC | |
| missing hint | int(arg) | SM_INTFNC | Defaults to int return type | 
| other than the above | - | - | not registered | 
Below are some examples that should help to explain how to define Python hook functions so that they are callable from Panther (e.g. JPL) with the desired argument types and return types:
# No type hint - default is int function; returns 2
def myFunc1():
        return 2
# No type hint - returns 2, converting return string to an int
def myFunc2():
        return "2"
# No type hint - returns 2, converting return float to an int
def myFunc3():
        return 2.6
# No type hint - raises and displays a TypeError; returns -1
def myFunc4():
        return "Hello"
# returns arg1, an int
def myFunc5(arg1:int, arg2:str="Default"):
        return arg1
# returns 0, same as a built-in C functions that return void
def myNoneFunc1()->None:
        pass
# returns 0; return statement's value is ignored
def myNoneFunc2()->None:
        return 2
# returns 2
def myIntFunc()->int:
        return 2
# returns arg as an int
def myIntFunc(arg:int)->int:
        return arg
# returns arg2 as a string; returns "default" if only arg1 is passed in
def myIntStrFunc(arg1:int, arg2:str="default")->str:
        return arg2
# missing type hint for arg1; not registered - not Panther-callable
def myBadFunc1(arg1):
        return arg1
# missing type hint for arg2; not registered - not Panther-callable
def myBadFunc2(arg1:int, arg2)->int:
        return arg1
When prototyped C functions, such as Panther Library Functions, are called from Python code, their protoypes determine type conversions in much the same manner.
When Python import statements are used, Panther resolves module names using a custom loader. The search order is:
The same search order is used in locating appinit.py, except that there is no fallback to standard Python module paths.
This mechanism allows you to package Python source files directly within Panther libraries, or outside of them, alongside screens, JPL, and other Panther application files. Precompiled .pyc files are also supported but discouraged due to potential compatibility issues with future Python interpreter versions.
Modules that are custom loaded from any of the three special locations in the search order have special ways in which they can be used. Their functions are registered using their full module names, and must be referenced that way in control strings and other Panther expressions. For example, to call a Python method that is within an imported myscreen module from a control string, use ^myscreen.myfunc(). If myfunc is defined directly within the Python screen code, then ^myfunc() should be used instead.
The names of widgets on the current screen are global during execution of the top level script within custom loaded modules. The Screen instance is also available as the self variable, the same as for Python code embedded within the screen itself. This makes it easy to move code that is embedded within a screen into an imported module, provided that such code is to be executed only once. Python ‘import’ statements are like JPL ‘public’ statements, not JPL ‘include’ statements.
Panther provides several built-in Python classes to simplify scripting:
App, Screen, Field, Widget, and ContainerWidgetPantherObject, PantherContainer and ArrayThe Field class, in particular, extends both Widget and Array. Array itself extends collections.abc.MutableSequence.
Occurrence access is 1-based, the same as in JPL. Default iteration is from 1 through the highest allocated occurrence, which will include any trailing empty occurrences. The len() method, however, returns the highest numbered non-empty occurrence.
The class hierarchy for Field looks like this:
Field → Array → MutableSequence
      ↘ Widget (1st in the MRO)
This dual inheritance allows Python code to treat Panther fields both as widgets and as arrays of values.
To retrieve or set the first occurrence’s value:
myfield.value = "New Value"value = myfield.value
‘value’ is a pseudo-property of Field, provided for convenience. This is equivalent to:
myfield[1] = "New Value"value = myfield[1]
The Field class overrides the default __str__() method to return the value within a field as a string.  Wherever Python will automatically convert a Field instance into a str, it will use the value within that field. For example:
myfield.value = "String Value"value = f"{myfield}" # assigns "String Value" to the variable
A field’s c_type and/or its data formatting property is used in determining which Python value types to accept or to retrieve. The default is to assign and retrieve a str value. A field should have a c_type of int, for example, when assigning a Python int:
count_field.value = 10
A field with a data_formatting property of numeric and a format_type of local_currency:
amount_field.value = 10.202
Panther displays$10.20
To retrieve a widget reference by name or field number:
field_ref = self["total$"]field_ref = self["#2"] # Field number
Field names that contain characters like $, and unnamed fields, can be referenced only by field number, using Panther’s ‘#’ operator. They must be accessed using self[...] syntax. Direct reference by name is available only if the name is a valid Python identifier:
field_ref = customer_id
myfield[1] = "John"           # Set the first occurrence to "John"
name = myfield[2]              # Retrieve the second occurrence from myfield
del myfield[3]                 # Remove the third occurrence
myfield[4:7] = ["A", "B", "C"] # Replace occurrences 4,5,6 with new values
values = myfield[1:5]          # Get occurrences 1 through 4
del myfield[2:4]               # Delete occurrences 2 and 3
myfield[-1] = "Last"           # Update last occurrence using negative index
entry = myfield[-2]            # Access the second-to-last occurrence
del myfield[-3]                # Remove the third-to-last occurrence
myfield.append("New Entry")    # Add a new occurrence to the end
count = len(myfield)           # Get the number of occurrences of myfield
value = myfield.pop()          # Remove and return last item
myfield.remove("old")          # Remove first occurrence of "old"
myfield.reverse()              # Reverse the order of items
myfield.reverse(len(myfield)+1)# non-standard reverse with custom end
reversed = myfield[::-1]       # assign a reverse slice
myfield.sort()                 # Uses sm_obj_sort()- key arg not supported
myfield.clear()                # Uses sm_1clear_array()
i = myfield.index("John")      # assign occurrence number of 1st "John" value
Some Array methods are implemented in Python rather than in C and use Python iteration and individual calls to a Panther API for each index. These methods should not be expected to be as performant as native Python list methods. Also, default iteration extends to the last allocated slot in an array. In other words, when looping through myfield (e.g., for item in myfield:), when myfield contains empty trailing occurrences, iteration goes beyond len(myfield). Use a slice on myfield if that isn’t desired (e.g., for item in myfield[1:len(myfield)]:).
Some Panther widgets are not fields. A Static Label widget, for example, is represented as an instance of Panther’s Widget class. A Panther Box, Grid Frame, Sync Group, Selection Group, Tab Deck, Tab Card, or Table View is represented as an instance of the ContainerWidget class, which is an extension of both Widget and PantherContainer. The Screen class also extends PantherContainer. Objects inheriting from PantherContainer can be traversed using standard Python coding techniques for traversing the members of a Python dictionary. The keys are Panther object IDs, and the values are instances of the various classes already discussed.
The following code prints the names of the fields within a Grid Frame widget named mygrid:
for field in mygrid.values():print(field.name)
| Panther Widget | Python Class | Notes | 
|---|---|---|
| Static Label | Widget | label property and others, but no value; no indexing | 
      
| Horizontal Line | Widget | property access, but no indexing | 
| Vertictal Line | Widget | Same as above | 
| Link | Widget | Same as above | 
| ActiveX | Widget | Same as above | 
| Graph | Widget | Same as above | 
| Box | ContainerWidget | Inherits from Widget and PantherContainer | 
| Grid Frame | ContainerWidget | Same as above | 
| Sync Group | ContainerWidget | Same as above | 
| Selection Group | ContainerWidget | Same as above | 
| Tab Deck | ContainerWidget | Same as above | 
| Tab Card | ContainerWidget | Same as above | 
| Table View | ContainerWidget | Same as above | 
All Panther widget and field properties are exposed using Python’s dot-syntax:
submit_button.label = "Submit"submit_button.enabled = False
Property values are type-converted upon assignment and retrieval, based upon the apparent types for their values, since Panther provides no data-type information for its properties.  Implicit data-type conversion works with str, int or float.
To set a field’s default foreground color:
myfield.fg_color_type = PV_BASICmyfield.fg_color_num = GREEN
This establishes a default appearance across all occurrences. Some properties, such as this one, can be overridden by element and/or occurrence number for more granular control. For example:
Properties(myfield)[1].fg_color_num = GREEN
This sets the foreground color for the first occurrence only. The fg_color_num property of element 1 can also be assigned:
ElementProperties(myfield)[1].fg_color_num = GREEN
Slices for Properties and ElementProperties are not supported.
# Test if running in Panther Web Application Broker:
if _app.in_web:
# Set a screen title:
self.title = "My Screen"
# Access field data:
customer_id.value = "12345"
order_total[1] = 99.95
# Set widget properties at runtime:
submit_button.enabled = True
status_label.value = "Complete"
# Assign a pushbutton’s control string dynamically:
submit_button.control_string = "^submit_form()"
# Set the length of all fields within a Grid Frame widget to 5:
for fld in mygrid.values():
    fld.length = 5
# Add up the values in a field named price, and assign the total to a field
# named total:
total.value = sum(price)
# Add up the total, assigning discounts to the prices in the price column of
# the price_table Grid Frame widget, using discount percentages from the
# discount column in the same Grid Frame:
total.value = 0
for price, discount in zip(price_table['price'](1,len(price)), price_table['discount']):
    discounted_price = price * (1 - discount / 100)
    total.value += discounted_price
Python code does not support embedded SQL as in JPL. Instead, use Panther’s dm_* Library functions:
sql = f"select * from customers where id = {customer_id.value}"dm_exec_sql(sql)
Be sure the SQL string uses syntax that Panther can parse (e.g., :+fieldname expansion). Results cannot be placed directly into Python variables—only into Panther fields.
Panther includes several Python modules in its runtime library, prorun5.lib:
smloader.pysmglobs.pysmpython.pysmprintbx.pyThe first three are executed during application initialization, and function in concert with C code in the Panther framework. smloader contains the custom class loader. smglobs provides global access to Panther constants and C prototyped function. smpython implements interoperability with Python screen and application code, and provides the Python classes that mirror Panther objects and paradigms. They are all available for reference, but undocumented features should not be relied upon, as they are subject to change in future releases of Panther.
smprintbx replaces the standard Python print function in builtins with an implementation that calls Panther’s sm_message_box API. It is most useful for Panther Client for Windows, which does not implement a console to which stdout messages can be written. Panther applications must import this module in order to use it – appinit.py is a logical place to import it.
Both standard Python and standard Panther techniques for internationalization can be used by application code. Additionally, when assigning strings to fields and properties, and when passing strings to prototyped C functions, Panther will automatically convert Python strings using the single-byte-per-character encoding specified by the PR_CODESET application property. It will do the reverse when retrieving string values from fields and properties, and when calling Python hook functions. Be aware that some runtime errors that are reported by Panther’s Python framework do not come from Panther’s message file, and cannot be internationalized in a standard way.
Standard Python debugging techniques can be used with Panther. To enable debugging, add the following code directly within appinit.py:
import sys
sys.executable = r"C:\Python311\python.exe"
import debugpy
debugpy.listen(("localhost", 9092))  # Port can be any open one
debugpy.wait_for_client()   # Waits until VS Code connects
debugpy.breakpoint()        # Optional: drops into debugger here
sys.executable should be assigned the path to your Python interpreter executable, if other than the path shown above. This assignment is a critical distinction that is specifically required only for debugging Python code in Panther. The other statements using debugpy are fairly standard, and variations on their usage can also work.
If debugpy is not available, use ‘pip install debugpy’ from a command shell to install it. You may also wish to place ‘import smprintbx’ in front of the code shown above.
To debug using Microsoft Visual Studio Code, make sure the Python Debugger extension is installed. Modify the launch.json configuration for VS Code by adding a configuration for debugging Panther applications.
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Attach to Panther",
            "type": "debugpy",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 9092
            },
            "connectTimeout": 30
        }
    ]
}
The port number shown above, 9092, can be any available port number, but it must match the one used in the call to debugpy.listen() in appinit.py.
After making the changes to appinit.py, when you start Panther, you will see "(Not Responding)" in Panther’s titlebar, and Panther will wait indefinitely for you to connect to it in VS Code. If you fail to connect with VS Code, you will need to kill the Panther process in Task Manager to end it, unless you implement a timeout mechanism around the debugpy.listen() call.
In VS Code, select the "Run and Debug" icon along the vertical left edge of the IDE. You should see a little green triangle "play" button below the menubar of VS Code, in the left-side panel. There is a dropdown next to the green triangle. Make sure the "Python: Attach to Panther" option has been selected there. Then, click the green triangle to connect to Panther. After it connects, you will see some warning messages about frozen columns. You may ignore the warnings and click OK. The warnings are for precompiled system classes that come with Python 3.11 and later, in order to improve start-up performance. Since you shouldn’t need to debug system source code, the warnings can be ignored.
Once connected, you can open any script that will be imported, and set breakpoints. To debug a screen’s Python code directly, you must save the code to a Python file whose base-name is the name of the screen (e.g., myscreen.jam.py). You can then open that file in the debugger and set breakpoints. The file won’t be used by Panther, but it will enable the debugger to track the source code lines that Panther is executing.
Modules that are custom loaded are reloaded whenever switching between Test Mode and Edit Mode in the Screen Editor. This enables one to make changes to Python source files and test them, without restarting Prodev. Debugging and editing external Python source files that Panther imports from SMPATH or the current working directory, is a convenient way to use VS Code during application development. Once development is completed, these source files can be moved into Panther libraries for application deployment.
ASCII screen files now support embedded Python code using PYTEXT= directives. Converting back to binary does not compile the Python code, which may result in invalid syntax being preserved in the binary screen file.
❓ Why is the screen object called self? Why not screen?
🟢 Answer: In Python, self is equivalent to  this in languages like Java or C++. It refers to the current instance of the class. While self is just a convention and any name could technically be used, Panther scripts act as methods of the Screen instance. Since there's no place to define parameters for these scripts, Panther automatically defines a self variable to reference the current screen instance.
❓ If the script is like a method for the Screen instance, why do functions in the script have access to self without being defined with a self parameter?
🟢 Answer: Functions in the screen's Python script behave like nested functions inside an unnamed outer function (the script itself). As nested functions, they inherit access to variables like self defined in the enclosing scope.
❓ When I execute:
Properties(myfield)[1].fg_color_num = GREEN
ElementProperties(myfield)[1].fg_color_num = BLUE
...my widget still shows the green foreground color. Why?
🟢 Answer: The occurrence property (Properties) takes precedence over the element property (ElementProperties). That’s why the green color remains visible.
❓ What does this do?
Properties(myfield)[0].fg_color_num = GREEN
🟢 Answer: This behaves like  myfield.fg_color_num = GREEN, but it is undocumented behavior and may change. Panther allows myfield[0] to reference the base field in non-Python code (e.g., in JPL), and this leniency was intentional to accommodate common coding errors.
❓ Why do dictionary keys for a Screen or ContainerWidget appear as Panther object IDs when looping, but square-bracket notation uses widget names?
🟢 Answer: Panther uses object IDs as dictionary keys because every widget has one, even unnamed ones. However, since object IDs vary between runs, Panther overrides __getitem__ and __setitem__ to allow referencing widgets by name, field number, or object ID using square brackets. For example:
myfield['#2']
❓ Why are functions in appinit.py global, but variables are not?
🟢 Answer: Functions need to be callable from Panther outside Python, while variables are only used within Python code. You can still access both by importing appinit.py, but only functions are made global by design.
❓ Why can't I assign a value to a field like in JPL (e.g., myfield = "Hello")?
🟢 Answer: In Python, myfield is an instance of the  Field class. Assigning "Hello" to it simply rebinds the variable to a string, not the field’s value. Python doesn’t allow overriding assignment behavior, so Panther can’t intercept that to update the field internally.
❓ Can I create a Field instance using its constructor?
🟢 Answer: Yes, but it's rarely necessary. You can do:
myfield_copy = Field(myfield.objid)
Or make a shallow copy:
myfield_copy = copy.copy(myfield)
These create new Python objects referencing the same Panther widget, not new widgets. Alternatively, you can just create a new reference:
myfield_ref = myfield
❓ How can I reference @-variables like @dmrowcount?
🟢 Answer: Use Panther’s API functions:
• For integers:
sm_n_intval('@dmrowcount')
• For strings:
sm_n_fptr('@dmengerrmsg')
These work with screen, widget, and property references, as well as with globals—but not with @-functions like @sum(). Panther’s @-functions can be called from Python using sm_calc():
• For functions:
sm_calc(0, 0, 'total = @sum(numbers)')
total and numbers must be fields or global JPL variables.