Migrate Jython Script to Python 3 Script (Container)
This guide helps Release administrators and plugin developers migrate automation from the Jython Script task to the Python 3 Script (Container) task.
The Jython Script task (xlrelease.ScriptTask) runs inline Python 2 automation inside a release: reading and updating releases, phases, tasks, and variables, calling the Release API, and making HTTP calls. The Python 3 Script (Container) task provides the same capabilities on a modern Python 3 runtime.
For most scripts the change is mechanical. The helper APIs and the context helpers keep the same names and camelCase methods as Jython. The main work is updating Python 2 syntax, replacing reserved variables with helper calls, swapping HttpRequest for requests, removing Java imports, and returning values through result, result_2, and result_3.
This page covers the common cases. For the complete reference, see the Jython to Python 3 migration guide on GitHub. A community helper tool can also automate parts of the conversion.
Why Migrate
The Jython Script task is not deprecated, and existing Jython scripts continue to work, so you can migrate at your own pace. Migrating is recommended because Jython is Python 2.7, which reached end of life in 2020. The Python 3 Script (Container) task runs on Python 3.12 in its own container through the Release Runner, so it can use standard Python packages (such as requests and pydantic) and returns strongly typed Pydantic models instead of Java proxies.
Prerequisites
Before you migrate a script, make sure you have:
- The Python3 Script (Container) plugin installed. For more information, see Python 3 Script (Container) task.
- A configured Digital.ai Release Runner to run container tasks.
- A run-as user set on the release or template. Every API and helper call runs as this user, so API calls fail if it is not set. Set the Run automated tasks as user property. For more information, see Create a Jython Script task.
Architecture Differences
Where the script runs explains every compatibility difference in this guide.
| Aspect | Jython Script | Python 3 Script (Container) |
|---|---|---|
| Task type id | xlrelease.ScriptTask | containerPython.PythonTask |
| Runtime | Jython (Python 2.7 on the JVM) | Python 3.12 |
| Execution location | In-process, inside the Release server | Separate container through the Release Runner |
| API access | In-process Java objects | Release REST API through the bundled client |
| Identity | The Release server process | The release's run-as user |
| Java interop | Yes (java.*, javax.*) | No |
| Sandbox | Yes (restricted file, network, classes) | No sandbox; an ephemeral container |
| Third-party packages | JVM and Java libraries | Python packages baked into the image |
| Return values | Configured task output properties | result, result_2, result_3 |
print() output | Task log | Container log and a single task comment |
In Jython, the API objects live in the server process. In the container, they are REST clients that call back as the run-as user. That difference is why directly bound objects (release, releaseVariables, and others) become helper-function calls.
Compatibility Overview
This table summarizes what changes for each capability when you move a script to the Python 3 Script (Container) task.
| Capability | Jython | Python 3 Script | Change required |
|---|---|---|---|
| Release helper APIs | Yes | Yes, same names and methods | None |
Domain model imports (com.xebialabs.xlrelease.domain.*) | Yes | Yes, same namespace | Usually none |
Context helpers (getCurrentRelease(), ...) | Yes | Yes | None |
Variable helpers (getReleaseVariable(), ...) | Yes | Yes | None |
Reserved objects (release, phase, task) | Yes | No | Use getCurrentRelease() and similar |
Reserved dicts (releaseVariables, globalVariables, folderVariables) | Yes | No | Use getter and setter helpers |
HttpRequest / HttpResponse | Yes | No | Use requests |
Java imports (java.*, javax.*) | Yes | No | Use Python equivalents |
| Python 2 syntax | Yes | No | Use Python 3 syntax |
| Custom task output properties | Yes | Three fixed names | Map to result, result_2, result_3, or release variables |
Migration Checklist
Work through this per script:
- Create a Python 3 Script (Container) task.
- Confirm the release has a run-as user with the required permissions.
- Convert Python 2 syntax to Python 3.
- Replace reserved variables with helper functions.
- Replace
HttpRequestwithrequests. - Remove
java.*andjavax.*imports. - Verify every third-party import is available in the container image.
- Map old output properties to
result,result_2,result_3, or release variables. - Keep Release API calls as they are.
- Run end to end with a container runner attached, then review the task comment and log.
Both task types can coexist, so you can convert one task or template at a time and validate as you go.
Reserved Variables to Helper Functions
Jython binds several objects and dictionaries directly into the script namespace. The Python 3 Script does not bind these. It exposes helper functions that fetch the data over the API on demand. Resolution is lazy, so a script that never references a helper makes no API request.
| Jython (reserved) | Python 3 Script (helper) | Notes |
|---|---|---|
release | getCurrentRelease() | Returns a typed Release |
phase | getCurrentPhase() | Returns a typed Phase |
task | getCurrentTask() | Returns a typed Task |
| (enclosing folder) | getCurrentFolder() | Returns a typed Folder |
releaseVariables["x"] | getReleaseVariable("x") | Raises KeyError if missing |
releaseVariables["x"] = v | setReleaseVariable("x", v) | Creates the variable if absent |
folderVariables["folder.x"] | getFolderVariable("folder.x") | folder. prefix required |
globalVariables["global.x"] | getGlobalVariable("global.x") | global. prefix required |
Jython:
# 'release' and 'releaseVariables' are bound into the script
print "Release: %s (%s)" % (release.title, release.status)
buildNumber = releaseVariables["buildNumber"]
releaseVariables["deployTarget"] = "production"
Python 3 Script:
release = getCurrentRelease()
print(f"Release: {release.title} ({release.status})")
buildNumber = getReleaseVariable("buildNumber")
setReleaseVariable("deployTarget", "production")
Release Helper APIs
Every com.xebialabs.xlrelease.api.v1 helper object from Jython is available in the Python 3 Script under the same name, with the same camelCase methods. Each is created lazily and shares one authenticated client, so unused APIs cost nothing.
# Identical in Jython and Python 3 Script
release = releaseApi.getRelease(getCurrentRelease().id)
task = taskApi.getTask(getCurrentTask().id)
The most common objects are releaseApi, phaseApi, taskApi, folderApi, templateApi, configurationApi, variableApi, searchApi, and settingsApi. For the full list of API objects and their methods, see Digital.ai Release Python API Client.
Jython's repositoryService (reading shared configurations and global variables) maps to configurationApi here, for example configurationApi.getGlobalVariables() and configurationApi.getConfiguration(...). There is no repositoryService object.
Domain classes share the com.xebialabs.xlrelease.domain.* namespace with the Java and Jython API, so imports port across unchanged:
from com.xebialabs.xlrelease.domain.release import Release
from com.xebialabs.xlrelease.domain.task import Task
Returned objects are Pydantic models. Read attributes, modify them, and pass them back to an update method.
Working with Variables
The variable helpers replace Jython's releaseVariables, folderVariables, and globalVariables dictionaries. A getter returns the typed value and raises KeyError if the variable does not exist. A setter updates the variable, or creates it when missing, inferring the type from the Python value.
Setters infer the Release variable type from the Python value:
| Python value | Release variable type |
|---|---|
bool | xlrelease.BooleanVariable |
int | xlrelease.IntegerVariable |
datetime or date | xlrelease.DateVariable |
dict | xlrelease.MapStringStringVariable |
list, set, or tuple | xlrelease.SetStringVariable |
anything else (str, ...) | xlrelease.StringVariable |
Release variables use bare names. Folder variables require a folder. prefix, and global variables require a global. prefix. Omitting a required prefix raises ValueError.
from datetime import datetime, timezone
setReleaseVariable("deployTarget", "production") # String
setReleaseVariable("retryCount", 3) # Integer
setReleaseVariable("dryRun", True) # Boolean
setReleaseVariable("approvers", ["alice", "bob"]) # Set
setReleaseVariable("config", {"env": "prod"}) # Map
target = getReleaseVariable("deployTarget") # 'production'
setFolderVariable("folder.buildVersion", "1.2.3")
setGlobalVariable("global.maintenanceMode", False)
A password-type variable never returns its stored secret. The Release REST API masks the value server-side, so the variable getters return the literal string ********. The resolved value map from releaseApi.getVariableValues(...) omits password variables entirely. There is no REST call that returns the plaintext.
HttpRequest to requests
Jython's HttpRequest and HttpResponse classes are not available. Use the bundled requests library, which ships in the image because it backs the Release API client.
| Jython | requests |
|---|---|
HttpRequest(params) | requests.Session() or per-call arguments |
request.get(path) | requests.get(url) |
request.post(path, body, contentType=...) | requests.post(url, json=...) or data=... |
response.getStatus() | response.status_code |
response.getResponse() | response.text or response.json() |
response.isSuccessful() | response.ok or response.raise_for_status() |
import requests
response = requests.get(
"https://api.example.com/users",
auth=("user", "pass"),
headers={"Accept": "application/json"},
timeout=30,
)
response.raise_for_status() # turn 4xx and 5xx into a task error
data = response.json()
Always set timeout=. Without it, a slow or unreachable host hangs the task until the runner kills it. A Jython HttpRequest often pulled its URL and credentials from an HTTP Server shared configuration. The container cannot read those shared configurations directly, so pass the values your script needs as release variables or task inputs.
Python 2 to Python 3 Syntax
Jython is Python 2.7 and the container runs Python 3.12. Port the common Python 2 constructs:
| Concern | Jython (2.7) | Python 3 Script |
|---|---|---|
print "Hi", name | print("Hi", name) | |
| Exceptions | except Exception, e: | except Exception as e: |
| Dict iteration | d.iteritems() | d.items() |
has_key | d.has_key("k") | "k" in d |
| Integer division | 5 / 2 (is 2) | 5 // 2 (is 2; / is float division) |
| Unicode | unicode(x), basestring | str(x), str |
| Ranges and input | xrange(), raw_input() | range(), input() |
map, filter, zip | return lists | return iterators; wrap in list(...) if needed |
Java Imports to Python
The container runs pure Python and cannot load Java classes. Remove every java.* and javax.* import and use a Python equivalent.
| Java class or API | Python 3 replacement |
|---|---|
java.util.Date, Calendar | datetime.datetime, datetime.date |
java.text.SimpleDateFormat | datetime.strftime and strptime |
java.lang.System.getenv | os.getenv |
java.io.File, java.nio.file.Path | pathlib.Path, os.path |
java.util.HashMap, ArrayList | dict, list |
java.util.UUID | uuid |
java.security.MessageDigest | hashlib |
java.net.HttpURLConnection | requests (bundled) |
org.json, Jackson | json (standard library) |
from datetime import datetime, timezone
import os, pathlib
now = datetime.now(timezone.utc)
home = pathlib.Path(os.getenv("HOME", "/tmp"))
Output Properties and print
The Python 3 Script exposes exactly three outputs. Assign these variable names:
result = "first value" # output property 'result'
result_2 = 42 # output property 'result_2'
result_3 = {"key": "value"} # output property 'result_3'
For more than three return values, or for structured output, write to release variables with setReleaseVariable(...) so downstream tasks can read them.
Everything printed with print(...) is echoed to the container log and posted as a single task comment when the script ends, even on failure. The task comment is visible in the UI, so never print secrets or large payloads. A raised exception fails the task with exit code 1 and reports the exception type, message, and a traceback trimmed to your script's own lines.
Common Patterns
Read a variable, do work, write a variable:
# Jython
build = releaseVariables["buildNumber"]
releaseVariables["artifactPath"] = "/builds/%s/app.jar" % build
# Python 3 Script
build = getReleaseVariable("buildNumber")
setReleaseVariable("artifactPath", f"/builds/{build}/app.jar")
Drive the Release API (largely unchanged):
# Jython
for t in releaseApi.getActiveTasks(release.id):
print t.title
# Python 3 Script
release_id = getCurrentRelease().id
for t in releaseApi.getActiveTasks(release_id):
print(t.title)
Unsupported Features
These Jython capabilities have no equivalent and must be removed or redesigned:
| Removed | Replacement |
|---|---|
Bound objects release, phase, task | getCurrentRelease(), getCurrentPhase(), getCurrentTask() |
Bound dicts releaseVariables, globalVariables, folderVariables | Getter and setter helpers |
HttpRequest / HttpResponse | requests |
| Java interop, reflection, JVM internals | Python equivalents |
| Reading shared-configuration secrets in-process | Release variables or task inputs |
| Persistent local files or server file-system access | Release variables, attachments, or an external store |
The container is ephemeral and isolated from the server file system. Do not assume files written by one task are visible to another task or the server.
Troubleshooting
The following table lists common errors, their causes, and how to fix them.
| Symptom | Cause | Fix |
|---|---|---|
| Cannot connect to Release API without server URL, username, or password | No run-as user on the release | Set a run-as user on the release or template |
403 Forbidden from an API call | Run-as user lacks a permission | Grant it, or run as a user who has it |
KeyError: No variable named X | Reading a missing variable | Check the name; create it first or guard with try/except KeyError |
ValueError: ... must include the 'folder.' prefix | Missing variable prefix | Use "folder.x" or "global.x" |
NameError: name 'release' is not defined | Using a Jython reserved object | Use the helper function |
ModuleNotFoundError: No module named 'xlrelease' or java | Using HttpRequest or a Java import | Use requests or a Python equivalent |
| Task hangs until the runner times out | HTTP call with no timeout | Add timeout= to every requests call |
Jython to Python 3 Cheat Sheet
This table maps common Jython constructs to their Python 3 Script (Container) equivalents.
| Jython | Python 3 Script (Container) |
|---|---|
xlrelease.ScriptTask | containerPython.PythonTask |
release / phase / task | getCurrentRelease() / getCurrentPhase() / getCurrentTask() |
releaseVariables["x"] | getReleaseVariable("x") |
releaseVariables["x"] = v | setReleaseVariable("x", v) |
folderVariables["folder.x"] | getFolderVariable("folder.x") |
globalVariables["global.x"] | getGlobalVariable("global.x") |
releaseApi, taskApi, phaseApi, ... | same names, same methods |
from xlrelease.HttpRequest import HttpRequest | import requests |
response.getStatus() | response.status_code |
print x | print(x) |
except Exception, e: | except Exception as e: |
from java.util import Date | from datetime import datetime |
| (configured task outputs) | result, result_2, result_3 |
Migration Helper Tool
The Jython to Python 3 Migrator is a community helper tool that automates parts of the conversion. It is not officially supported by Digital.ai. Review its output before running migrated scripts.