This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new dcdb867e96bb CAMEL-24467: Scripting languages - bind Exchange 
variables in JavaScript, MVEL, OGNL, SpEL, Python and Python3, and use the 
Groovy binding names in JavaScript, Python and Python3 (#27525)
dcdb867e96bb is described below

commit dcdb867e96bb897d97c6c30e8c40b0d2d233cf94
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 8 22:24:58 2026 +0530

    CAMEL-24467: Scripting languages - bind Exchange variables in JavaScript, 
MVEL, OGNL, SpEL, Python and Python3, and use the Groovy binding names in 
JavaScript, Python and Python3 (#27525)
    
    Groovy and Jactl bind Exchange variables as "variable" and "variables", but
    the other scripting languages did not, so a script could only reach 
variables
    through the exchange object (and not at all in python3's default data-only 
mode).
    
    - js and python: bind "variable" and "variables", and use the Groovy binding
      names (exchangeProperties, camelContext); the old names are deprecated.
    - mvel, ognl and spel (RootObject): add getVariables(), getVariable(name) 
and
      getVariable(name, type); getVariable resolves global:/route: variables.
    - python3 (GraalPy): bind "variables" as a data binding like "headers", in
      default and trusted mode, and only the Groovy names (new in 4.23.0, so no
      deprecated aliases). Host access is not widened.
    
    The language docs list the new bindings and the upgrade guide notes the
    deprecated js/python names.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/js-language.adoc |  22 +++-
 .../apache/camel/catalog/docs/mvel-language.adoc   |  16 ++-
 .../apache/camel/catalog/docs/ognl-language.adoc   |  16 ++-
 .../apache/camel/catalog/docs/python-language.adoc |  22 +++-
 .../camel/catalog/docs/python3-language.adoc       |  40 ++++--
 .../apache/camel/catalog/docs/spel-language.adoc   |  16 ++-
 .../src/main/docs/js-language.adoc                 |  22 +++-
 .../camel/language/js/JavaScriptExpression.java    |  29 ++++-
 .../language/js/JavaScriptBindingNamesTest.java    |  69 +++++++++++
 .../camel/language/js/JavaScriptVariablesTest.java | 100 +++++++++++++++
 .../camel-mvel/src/main/docs/mvel-language.adoc    |  16 ++-
 .../org/apache/camel/language/mvel/RootObject.java |  25 ++++
 .../camel/language/mvel/MvelVariablesTest.java     |  91 ++++++++++++++
 .../camel-ognl/src/main/docs/ognl-language.adoc    |  16 ++-
 .../org/apache/camel/language/ognl/RootObject.java |  25 ++++
 .../camel/language/ognl/OgnlVariablesTest.java     |  91 ++++++++++++++
 .../src/main/docs/python-language.adoc             |  22 +++-
 .../camel/language/python/PythonExpression.java    |  29 ++++-
 .../language/python/PythonBindingNamesTest.java    |  67 ++++++++++
 .../camel/language/python/PythonVariablesTest.java |  96 +++++++++++++++
 .../src/main/docs/python3-language.adoc            |  40 ++++--
 .../camel/language/python3/Python3Language.java    |  43 ++++---
 .../language/python3/Python3BindingNamesTest.java  | 126 +++++++++++++++++++
 .../language/python3/Python3LanguageEvalTest.java  |   2 +-
 .../python3/Python3LanguageSecurityTest.java       |   4 +-
 .../language/python3/Python3VariablesTest.java     | 135 +++++++++++++++++++++
 .../camel/language/spel/SpelVariablesTest.java     |  61 ++++++++++
 .../camel-spring/src/main/docs/spel-language.adoc  |  16 ++-
 .../org/apache/camel/language/spel/RootObject.java |  25 ++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   8 ++
 30 files changed, 1206 insertions(+), 84 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
index 7ddc740ab0ad..28f67ca33c24 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
 |=======================================================================
 |Variable |Type |Description
 |*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
 |exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
 |exchangeId |String |the exchange id
 |message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `null`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
 |=======================================================================
 
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language, except `in`, which is a
+reserved word in JavaScript (use `message` or `request`), and the deprecated 
`out` and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for 
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables, 
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
 
 == Dependencies
 
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
index f38cfd68f463..fe52fad1b704 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
@@ -57,13 +57,21 @@ The following Camel related variables are made available:
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`. 
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves 
global and route
+variables, such as `getVariable('global:foo')`, which are not in the 
`variables` map.
+
 == Example
 
 For example, you could use MVEL inside a xref:eips:filter-eip.adoc[Message
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
index acd232b4ccd4..5894057a024d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
@@ -56,13 +56,21 @@ include::partial$language-options.adoc[]
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`. 
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves 
global and route
+variables, such as `getVariable('global:foo')`, which are not in the 
`variables` map.
+
 == Example
 
 For example, you could use OGNL inside a xref:eips:filter-eip.adoc[Message
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
index c4d66ef8aa27..486c001e046f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
 |=======================================================================
 |Variable |Type |Description
 |*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
 |exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
 |exchangeId |String |the exchange id
 |message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `None`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
 |=======================================================================
 
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language, except `in`, which is a
+reserved word in Python (use `message` or `request`), and the deprecated `out` 
and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for 
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables, 
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
 
 == Dependencies
 
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
index 68b82eb9bd74..040caedfaef8 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
@@ -32,17 +32,35 @@ The following variables are bound by default:
 |=======================================================================
 |Variable |Type |Description
 |body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
 |exchangeId |String |the exchange id
 |=======================================================================
 
-By default, Python can index Java maps and lists (for example `headers['foo']`)
-but cannot invoke methods on host objects.
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language. In default mode only the
+data variables above are bound; the Camel host objects are bound only with 
trusted host access.
 
-`message`, `exchange`, and `context` are not bound in default mode. Scripts 
that
-refer to them raise a Python `NameError`. Those variables are available only 
when
-you opt in to trusted host access, as described in <<_trusted_host_access>>.
+By default, Python can index Java maps and lists (for example `headers['foo']`
+or `variables['foo']`) but cannot invoke methods on host objects.
+
+`variables` holds the exchange-scoped variables (for example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
+such as `global:foo`, are not in this map, so in default mode a script cannot 
read
+them; with trusted host access they can be read with 
`exchange.getVariable(...)`.
+Like `headers`, it is the exchange's own map rather than a copy, so an 
assignment
+such as `variables['foo'] = 'bar'` updates the exchange. Such an assignment 
does not
+go through `Exchange.setVariable`, so `variables['global:foo'] = 1` sets an 
exchange
+variable named `global:foo`, not a global one. Setting a variable to `None` 
fails,
+because the variables map does not accept null values.
+
+`exchange`, `camelContext`, `message`, `request` and `exception` are not
+bound in default mode. Scripts that refer to them raise a Python `NameError`. 
Those
+variables are available only when you opt in to trusted host access, as 
described in
+<<_trusted_host_access>>.
 
 == Security
 
@@ -72,8 +90,8 @@ you trust the scripts.
 [WARNING]
 ====
 Trusted host access lets Python call public methods on bound Camel objects 
such as
-`context`, `exchange`, and `message`. That includes destructive operations, for
-example `context.stop()`, `context.getRegistry().bind(...)`, and
+`camelContext`, `exchange`, and `message`. That includes destructive 
operations, for
+example `camelContext.stop()`, `camelContext.getRegistry().bind(...)`, and
 `exchange.getContext().getExecutorService(...)`. Enable this mode only for 
fully
 trusted scripts. Do not use it with Python code taken from untrusted or 
external
 input.
@@ -84,9 +102,11 @@ In this mode the default variables above remain available, 
plus:
 [width="100%",cols="10%,10%,80%",options="header",]
 |=======================================================================
 |Variable |Type |Description
-|message |Message |the message
 |exchange |Exchange |the Exchange
-|context |CamelContext |the CamelContext
+|camelContext |CamelContext |the CamelContext
+|message |Message |the message
+|request |Message |the message (same as `message`)
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `None`
 |=======================================================================
 
 [source,java]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
index b1b1e41c577f..76c0c583257f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
@@ -40,13 +40,21 @@ The following Camel related variables are made available:
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `#{getHeader('foo')}` or 
`#{getVariable('foo')}`. The maps can be
+read as `#{headers.foo}` or `#{variables['foo']}`. `getVariable` also resolves 
global and route
+variables, such as `#{getVariable('global:foo')}`, which are not in the 
`variables` map.
+
 == Example
 
 You can use SpEL as an expression for 
xref:eips:recipientList-eip.adoc[Recipient
diff --git a/components/camel-javascript/src/main/docs/js-language.adoc 
b/components/camel-javascript/src/main/docs/js-language.adoc
index 7ddc740ab0ad..28f67ca33c24 100644
--- a/components/camel-javascript/src/main/docs/js-language.adoc
+++ b/components/camel-javascript/src/main/docs/js-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
 |=======================================================================
 |Variable |Type |Description
 |*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
 |exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
 |exchangeId |String |the exchange id
 |message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `null`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
 |=======================================================================
 
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language, except `in`, which is a
+reserved word in JavaScript (use `message` or `request`), and the deprecated 
`out` and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for 
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables, 
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
 
 == Dependencies
 
diff --git 
a/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
 
b/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
index 5d360a053574..1c02b7689b30 100644
--- 
a/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
+++ 
b/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
@@ -16,8 +16,12 @@
  */
 package org.apache.camel.language.js;
 
+import java.util.Map;
+
 import org.apache.camel.Exchange;
+import org.apache.camel.Message;
 import org.apache.camel.support.ExpressionSupport;
+import org.apache.camel.support.LanguageHelper;
 import org.graalvm.polyglot.Context;
 import org.graalvm.polyglot.Value;
 
@@ -53,13 +57,28 @@ public class JavaScriptExpression extends ExpressionSupport 
{
         try (Context cx = lang.newContext()) {
             Value b = cx.getBindings("js");
 
+            Message message = exchange.getMessage();
+            Map<String, Object> headers = message.getHeaders();
+            Map<String, Object> properties = exchange.getAllProperties();
+            Map<String, Object> variables = exchange.getVariables();
+            // the names ExchangeHelper.populateVariableMap gives Groovy, 
except in (a reserved word) and the deprecated
+            // out and response
             b.putMember("exchange", exchange);
-            b.putMember("context", exchange.getContext());
+            b.putMember("camelContext", exchange.getContext());
             b.putMember("exchangeId", exchange.getExchangeId());
-            b.putMember("message", exchange.getMessage());
-            b.putMember("headers", exchange.getMessage().getHeaders());
-            b.putMember("properties", exchange.getAllProperties());
-            b.putMember("body", exchange.getMessage().getBody());
+            b.putMember("message", message);
+            b.putMember("request", message);
+            b.putMember("body", message.getBody());
+            b.putMember("header", headers);
+            b.putMember("headers", headers);
+            b.putMember("exchangeProperty", properties);
+            b.putMember("exchangeProperties", properties);
+            b.putMember("variable", variables);
+            b.putMember("variables", variables);
+            b.putMember("exception", LanguageHelper.exception(exchange));
+            // deprecated names of camelContext and exchangeProperties
+            b.putMember("context", exchange.getContext());
+            b.putMember("properties", properties);
 
             Value o = cx.eval(lang.source(expressionString));
             Object answer = JavaScriptLanguage.materialize(o);
diff --git 
a/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
 
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
new file mode 100644
index 000000000000..574bfa3bf77d
--- /dev/null
+++ 
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.js;
+
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+/**
+ * The script variables have the names {@code 
ExchangeHelper.populateVariableMap} gives Groovy, and the older
+ * {@code context} and {@code properties} still work as deprecated names.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class JavaScriptBindingNamesTest extends LanguageTestSupport {
+
+    @Test
+    void groovyNamesAreBound() {
+        exchange.setProperty("color", "red");
+        exchange.setVariable("foo", "bar");
+
+        assertExpression("exchange.getExchangeId() == exchangeId", true);
+        assertExpression("camelContext.getName()", context.getName());
+        assertExpression("message.getHeader('foo')", "abc");
+        assertExpression("request.getHeader('foo')", "abc");
+        assertExpression("body", "<hello id='m123'>world!</hello>");
+        assertExpression("header.foo", "abc");
+        assertExpression("headers.foo", "abc");
+        assertExpression("exchangeProperty.color", "red");
+        assertExpression("exchangeProperties.color", "red");
+        assertExpression("variable.foo", "bar");
+        assertExpression("variables.foo", "bar");
+    }
+
+    @Test
+    void exceptionIsBound() {
+        assertExpression("exception == null", true);
+
+        exchange.setException(new IllegalArgumentException("Forced"));
+        assertExpression("exception.getMessage()", "Forced");
+    }
+
+    @Test
+    void deprecatedNamesStillWork() {
+        exchange.setProperty("color", "red");
+
+        assertExpression("context.getName()", context.getName());
+        assertExpression("context === camelContext", true);
+        assertExpression("properties.color", "red");
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "js";
+    }
+}
diff --git 
a/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
 
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
new file mode 100644
index 000000000000..e21065b5e8ca
--- /dev/null
+++ 
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.js;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exchange variables are bound as {@code variable} and {@code variables}, 
next to {@code headers} and
+ * {@code properties}.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class JavaScriptVariablesTest extends LanguageTestSupport {
+
+    @Test
+    void variablesAreBound() {
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        assertExpression("variables.foo", "bar");
+        assertExpression("variables['foo']", "bar");
+        assertExpression("variable.get('foo')", "bar");
+        assertExpression("variables.num + 2", 7);
+        assertExpression("variables.get('missing')", null);
+        assertPredicate("variables.foo == 'bar'");
+        assertPredicate("variable.foo == 'baz'", false);
+    }
+
+    @Test
+    void variablesAreBoundForIndirectLookup() {
+        exchange.setVariable("foo", "bar");
+
+        assertExpression("globalThis['vari' + 'ables'].foo", "bar");
+        assertExpression("globalThis['vari' + 'able'].foo", "bar");
+    }
+
+    @Test
+    void variablesAreBoundWhenNoVariableIsSet() {
+        Language language = context.resolveLanguage("js");
+        Exchange exchange = new DefaultExchange(context);
+
+        assertThat(language.createExpression("globalThis['vari' + 
'ables'].size()").evaluate(exchange, Integer.class))
+                .isZero();
+    }
+
+    @Test
+    void variablesSetInRoute() throws Exception {
+        getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+        getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader("direct:start", "Camel", "greeting", 
"Hello");
+        template.sendBodyAndHeader("direct:start", "World", "greeting", 
"Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setVariable("name", body())
+                        .setVariable("greeting", header("greeting"))
+                        .setBody().js("variables.greeting + ' ' + 
variable.get('name')")
+                        .choice()
+                        .when().js("variables.name == 
'Camel'").to("mock:camel")
+                        .otherwise().to("mock:other");
+            }
+        };
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "js";
+    }
+}
diff --git a/components/camel-mvel/src/main/docs/mvel-language.adoc 
b/components/camel-mvel/src/main/docs/mvel-language.adoc
index f38cfd68f463..fe52fad1b704 100644
--- a/components/camel-mvel/src/main/docs/mvel-language.adoc
+++ b/components/camel-mvel/src/main/docs/mvel-language.adoc
@@ -57,13 +57,21 @@ The following Camel related variables are made available:
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`. 
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves 
global and route
+variables, such as `getVariable('global:foo')`, which are not in the 
`variables` map.
+
 == Example
 
 For example, you could use MVEL inside a xref:eips:filter-eip.adoc[Message
diff --git 
a/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
 
b/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
index bf2fc79bda38..60af18e4020d 100644
--- 
a/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
+++ 
b/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
@@ -81,4 +81,29 @@ public class RootObject {
     public <T> T getHeader(String name, Class<T> type) {
         return exchange.getMessage().getHeader(name, type);
     }
+
+    /**
+     * The variables of the exchange (exchange-scoped only; global and route 
variables are not in this map).
+     *
+     * @see #getVariable(String) to look up a global or route variable by its 
prefixed name
+     */
+    public Map<String, Object> getVariables() {
+        return exchange.getVariables();
+    }
+
+    /**
+     * The variable with the given name, which can be prefixed with {@code 
global:} or {@code route:} for a global or
+     * route variable.
+     */
+    public Object getVariable(String name) {
+        return exchange.getVariable(name);
+    }
+
+    /**
+     * The variable with the given name converted to the given type, which can 
be prefixed with {@code global:} or
+     * {@code route:} for a global or route variable.
+     */
+    public <T> T getVariable(String name, Class<T> type) {
+        return exchange.getVariable(name, type);
+    }
 }
diff --git 
a/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
 
b/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
new file mode 100644
index 000000000000..60dbd517ab96
--- /dev/null
+++ 
b/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.mvel;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables}, 
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code 
getHeader(...)}.
+ */
+class MvelVariablesTest extends LanguageTestSupport {
+
+    @Test
+    void variablesAreBound() {
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        assertExpression("variables.foo", "bar");
+        assertExpression("variables['foo']", "bar");
+        assertExpression("getVariable('foo')", "bar");
+        assertExpression("variables.num + 2", 7);
+        assertExpression("getVariable('missing')", null);
+        assertPredicate("variables.foo == 'bar'");
+        assertPredicate("getVariable('foo') == 'baz'", false);
+    }
+
+    @Test
+    void typedVariableGetterConverts() {
+        exchange.setVariable("num", "123");
+
+        assertExpression("getVariable('num', Integer) + 1", 124);
+        assertPredicate("getVariable('num', Integer) > 100");
+    }
+
+    @Test
+    void globalVariableThroughGetter() {
+        exchange.setVariable("global:greeting", "Hi");
+
+        assertExpression("getVariable('global:greeting')", "Hi");
+    }
+
+    @Test
+    void variablesSetInRoute() throws Exception {
+        getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+        getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader("direct:start", "Camel", "greeting", 
"Hello");
+        template.sendBodyAndHeader("direct:start", "World", "greeting", 
"Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setVariable("name", body())
+                        .setVariable("greeting", header("greeting"))
+                        .setBody().mvel("variables.greeting + ' ' + 
getVariable('name')")
+                        .choice()
+                        .when().mvel("variables.name == 
'Camel'").to("mock:camel")
+                        .otherwise().to("mock:other");
+            }
+        };
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "mvel";
+    }
+}
diff --git a/components/camel-ognl/src/main/docs/ognl-language.adoc 
b/components/camel-ognl/src/main/docs/ognl-language.adoc
index acd232b4ccd4..5894057a024d 100644
--- a/components/camel-ognl/src/main/docs/ognl-language.adoc
+++ b/components/camel-ognl/src/main/docs/ognl-language.adoc
@@ -56,13 +56,21 @@ include::partial$language-options.adoc[]
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`. 
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves 
global and route
+variables, such as `getVariable('global:foo')`, which are not in the 
`variables` map.
+
 == Example
 
 For example, you could use OGNL inside a xref:eips:filter-eip.adoc[Message
diff --git 
a/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
 
b/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
index f1ba1722330d..d323912cfa0a 100644
--- 
a/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
+++ 
b/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
@@ -78,4 +78,29 @@ public class RootObject {
         return exchange.getMessage().getHeader(name, type);
     }
 
+    /**
+     * The variables of the exchange (exchange-scoped only; global and route 
variables are not in this map).
+     *
+     * @see #getVariable(String) to look up a global or route variable by its 
prefixed name
+     */
+    public Map<String, Object> getVariables() {
+        return exchange.getVariables();
+    }
+
+    /**
+     * The variable with the given name, which can be prefixed with {@code 
global:} or {@code route:} for a global or
+     * route variable.
+     */
+    public Object getVariable(String name) {
+        return exchange.getVariable(name);
+    }
+
+    /**
+     * The variable with the given name converted to the given type, which can 
be prefixed with {@code global:} or
+     * {@code route:} for a global or route variable.
+     */
+    public <T> T getVariable(String name, Class<T> type) {
+        return exchange.getVariable(name, type);
+    }
+
 }
diff --git 
a/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
 
b/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
new file mode 100644
index 000000000000..af2dbaad2529
--- /dev/null
+++ 
b/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.ognl;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables}, 
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code 
getHeader(...)}.
+ */
+class OgnlVariablesTest extends LanguageTestSupport {
+
+    @Test
+    void variablesAreBound() {
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        assertExpression("variables.foo", "bar");
+        assertExpression("variables['foo']", "bar");
+        assertExpression("getVariable('foo')", "bar");
+        assertExpression("variables.num + 2", 7);
+        assertExpression("getVariable('missing')", null);
+        assertPredicate("variables.foo == 'bar'");
+        assertPredicate("getVariable('foo') == 'baz'", false);
+    }
+
+    @Test
+    void typedVariableGetterConverts() {
+        exchange.setVariable("num", "123");
+
+        assertExpression("getVariable('num', @java.lang.Integer@class) + 1", 
124);
+        assertPredicate("getVariable('num', @java.lang.Integer@class) > 100");
+    }
+
+    @Test
+    void globalVariableThroughGetter() {
+        exchange.setVariable("global:greeting", "Hi");
+
+        assertExpression("getVariable('global:greeting')", "Hi");
+    }
+
+    @Test
+    void variablesSetInRoute() throws Exception {
+        getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+        getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader("direct:start", "Camel", "greeting", 
"Hello");
+        template.sendBodyAndHeader("direct:start", "World", "greeting", 
"Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setVariable("name", body())
+                        .setVariable("greeting", header("greeting"))
+                        .setBody().ognl("variables.greeting + ' ' + 
getVariable('name')")
+                        .choice()
+                        .when().ognl("variables.name == 
'Camel'").to("mock:camel")
+                        .otherwise().to("mock:other");
+            }
+        };
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "ognl";
+    }
+}
diff --git a/components/camel-python/src/main/docs/python-language.adoc 
b/components/camel-python/src/main/docs/python-language.adoc
index c4d66ef8aa27..486c001e046f 100644
--- a/components/camel-python/src/main/docs/python-language.adoc
+++ b/components/camel-python/src/main/docs/python-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
 |=======================================================================
 |Variable |Type |Description
 |*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
 |exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
 |exchangeId |String |the exchange id
 |message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `None`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
 |=======================================================================
 
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language, except `in`, which is a
+reserved word in Python (use `message` or `request`), and the deprecated `out` 
and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for 
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables, 
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
 
 == Dependencies
 
diff --git 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
index 3de078780711..af1ab5077744 100644
--- 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
+++ 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
@@ -16,9 +16,13 @@
  */
 package org.apache.camel.language.python;
 
+import java.util.Map;
+
 import org.apache.camel.Exchange;
 import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.Message;
 import org.apache.camel.support.ExpressionSupport;
+import org.apache.camel.support.LanguageHelper;
 import org.python.core.PyCode;
 import org.python.core.PyObject;
 import org.python.util.PythonInterpreter;
@@ -57,13 +61,28 @@ public class PythonExpression extends ExpressionSupport {
 
     private <T> T doEvaluate(Exchange exchange, Class<T> type) {
         try {
+            Message message = exchange.getMessage();
+            Map<String, Object> headers = message.getHeaders();
+            Map<String, Object> properties = exchange.getAllProperties();
+            Map<String, Object> variables = exchange.getVariables();
+            // the names ExchangeHelper.populateVariableMap gives Groovy, 
except in (a reserved word) and the deprecated
+            // out and response
             compiler.set("exchange", exchange);
-            compiler.set("context", exchange.getContext());
+            compiler.set("camelContext", exchange.getContext());
             compiler.set("exchangeId", exchange.getExchangeId());
-            compiler.set("message", exchange.getMessage());
-            compiler.set("headers", exchange.getMessage().getHeaders());
-            compiler.set("properties", exchange.getAllProperties());
-            compiler.set("body", exchange.getMessage().getBody());
+            compiler.set("message", message);
+            compiler.set("request", message);
+            compiler.set("body", message.getBody());
+            compiler.set("header", headers);
+            compiler.set("headers", headers);
+            compiler.set("exchangeProperty", properties);
+            compiler.set("exchangeProperties", properties);
+            compiler.set("variable", variables);
+            compiler.set("variables", variables);
+            compiler.set("exception", LanguageHelper.exception(exchange));
+            // deprecated names of camelContext and exchangeProperties
+            compiler.set("context", exchange.getContext());
+            compiler.set("properties", properties);
 
             PyObject out = compiler.eval(compiledExpression);
             if (out != null) {
diff --git 
a/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
new file mode 100644
index 000000000000..776e443b68de
--- /dev/null
+++ 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.python;
+
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The script variables have the names {@code 
ExchangeHelper.populateVariableMap} gives Groovy, and the older
+ * {@code context} and {@code properties} still work as deprecated names.
+ */
+class PythonBindingNamesTest extends LanguageTestSupport {
+
+    @Test
+    void groovyNamesAreBound() {
+        exchange.setProperty("color", "red");
+        exchange.setVariable("foo", "bar");
+
+        assertExpression("exchange.getExchangeId() == exchangeId", true);
+        assertExpression("camelContext.getName()", context.getName());
+        assertExpression("message.getHeader('foo')", "abc");
+        assertExpression("request.getHeader('foo')", "abc");
+        assertExpression("body", "<hello id='m123'>world!</hello>");
+        assertExpression("header['foo']", "abc");
+        assertExpression("headers['foo']", "abc");
+        assertExpression("exchangeProperty['color']", "red");
+        assertExpression("exchangeProperties['color']", "red");
+        assertExpression("variable['foo']", "bar");
+        assertExpression("variables['foo']", "bar");
+    }
+
+    @Test
+    void exceptionIsBound() {
+        assertExpression("exception is None", true);
+
+        exchange.setException(new IllegalArgumentException("Forced"));
+        assertExpression("exception.getMessage()", "Forced");
+    }
+
+    @Test
+    void deprecatedNamesStillWork() {
+        exchange.setProperty("color", "red");
+
+        assertExpression("context.getName()", context.getName());
+        assertExpression("context == camelContext", true);
+        assertExpression("properties['color']", "red");
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "python";
+    }
+}
diff --git 
a/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
new file mode 100644
index 000000000000..29b9d8e928cd
--- /dev/null
+++ 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.python;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Exchange variables are bound as {@code variable} and {@code variables}, 
next to {@code headers} and
+ * {@code properties}.
+ */
+class PythonVariablesTest extends LanguageTestSupport {
+
+    @Test
+    void variablesAreBound() {
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        assertExpression("variables['foo']", "bar");
+        assertExpression("variable['foo']", "bar");
+        assertExpression("variables['num'] + 2", 7);
+        assertExpression("variables.get('missing') is None", true);
+        assertPredicate("variables['foo'] == 'bar'");
+        assertPredicate("variables['foo'] == 'baz'", false);
+    }
+
+    @Test
+    void variablesAreBoundForIndirectLookup() {
+        exchange.setVariable("foo", "bar");
+
+        assertExpression("globals()['vari' + 'ables']['foo']", "bar");
+        assertExpression("globals()['vari' + 'able']['foo']", "bar");
+    }
+
+    @Test
+    void variablesAreBoundWhenNoVariableIsSet() {
+        Language language = context.resolveLanguage("python");
+        Exchange exchange = new DefaultExchange(context);
+
+        assertEquals("0", language.createExpression("str(len(globals()['vari' 
+ 'ables']))").evaluate(exchange, String.class));
+    }
+
+    @Test
+    void variablesSetInRoute() throws Exception {
+        getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+        getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader("direct:start", "Camel", "greeting", 
"Hello");
+        template.sendBodyAndHeader("direct:start", "World", "greeting", 
"Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setVariable("name", body())
+                        .setVariable("greeting", header("greeting"))
+                        .setBody().python("variables['greeting'] + ' ' + 
variable['name']")
+                        .choice()
+                        .when().python("variables['name'] == 
'Camel'").to("mock:camel")
+                        .otherwise().to("mock:other");
+            }
+        };
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "python";
+    }
+}
diff --git a/components/camel-python3/src/main/docs/python3-language.adoc 
b/components/camel-python3/src/main/docs/python3-language.adoc
index 68b82eb9bd74..040caedfaef8 100644
--- a/components/camel-python3/src/main/docs/python3-language.adoc
+++ b/components/camel-python3/src/main/docs/python3-language.adoc
@@ -32,17 +32,35 @@ The following variables are bound by default:
 |=======================================================================
 |Variable |Type |Description
 |body |Object |the message body
+|header |Map |the message headers (same as `headers`)
 |headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
 |exchangeId |String |the exchange id
 |=======================================================================
 
-By default, Python can index Java maps and lists (for example `headers['foo']`)
-but cannot invoke methods on host objects.
+The variables have the same names as in the xref:groovy-language.adoc[Groovy] 
language. In default mode only the
+data variables above are bound; the Camel host objects are bound only with 
trusted host access.
 
-`message`, `exchange`, and `context` are not bound in default mode. Scripts 
that
-refer to them raise a Python `NameError`. Those variables are available only 
when
-you opt in to trusted host access, as described in <<_trusted_host_access>>.
+By default, Python can index Java maps and lists (for example `headers['foo']`
+or `variables['foo']`) but cannot invoke methods on host objects.
+
+`variables` holds the exchange-scoped variables (for example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
+such as `global:foo`, are not in this map, so in default mode a script cannot 
read
+them; with trusted host access they can be read with 
`exchange.getVariable(...)`.
+Like `headers`, it is the exchange's own map rather than a copy, so an 
assignment
+such as `variables['foo'] = 'bar'` updates the exchange. Such an assignment 
does not
+go through `Exchange.setVariable`, so `variables['global:foo'] = 1` sets an 
exchange
+variable named `global:foo`, not a global one. Setting a variable to `None` 
fails,
+because the variables map does not accept null values.
+
+`exchange`, `camelContext`, `message`, `request` and `exception` are not
+bound in default mode. Scripts that refer to them raise a Python `NameError`. 
Those
+variables are available only when you opt in to trusted host access, as 
described in
+<<_trusted_host_access>>.
 
 == Security
 
@@ -72,8 +90,8 @@ you trust the scripts.
 [WARNING]
 ====
 Trusted host access lets Python call public methods on bound Camel objects 
such as
-`context`, `exchange`, and `message`. That includes destructive operations, for
-example `context.stop()`, `context.getRegistry().bind(...)`, and
+`camelContext`, `exchange`, and `message`. That includes destructive 
operations, for
+example `camelContext.stop()`, `camelContext.getRegistry().bind(...)`, and
 `exchange.getContext().getExecutorService(...)`. Enable this mode only for 
fully
 trusted scripts. Do not use it with Python code taken from untrusted or 
external
 input.
@@ -84,9 +102,11 @@ In this mode the default variables above remain available, 
plus:
 [width="100%",cols="10%,10%,80%",options="header",]
 |=======================================================================
 |Variable |Type |Description
-|message |Message |the message
 |exchange |Exchange |the Exchange
-|context |CamelContext |the CamelContext
+|camelContext |CamelContext |the CamelContext
+|message |Message |the message
+|request |Message |the message (same as `message`)
+|exception |Exception |the exception if the exchange failed (or the caught 
exception in an error handler), otherwise `None`
 |=======================================================================
 
 [source,java]
diff --git 
a/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
 
b/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
index 695dd08d9582..f8f15d99c31b 100644
--- 
a/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
+++ 
b/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
@@ -32,12 +32,14 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.ExpressionEvaluationException;
 import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.Message;
 import org.apache.camel.Predicate;
 import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.Service;
 import org.apache.camel.spi.ScriptingLanguage;
 import org.apache.camel.spi.annotations.Language;
 import org.apache.camel.support.LRUCacheFactory;
+import org.apache.camel.support.LanguageHelper;
 import org.apache.camel.support.TypedLanguageSupport;
 import org.graalvm.polyglot.Context;
 import org.graalvm.polyglot.Engine;
@@ -50,10 +52,11 @@ import org.graalvm.polyglot.Value;
  * Camel expression language for Python 3 via <a 
href="https://www.graalvm.org/python/";>GraalPy</a>.
  *
  * <p>
- * Default scripts see only data bindings: {@code body}, {@code headers}, 
{@code properties}, and {@code exchangeId}.
- * {@code exchange}, {@code message}, and {@code context} are intentionally 
absent so they resolve as Python
- * {@code NameError} rather than opaque objects with no usable API. Binding 
them by default would also become a
- * privilege escalation if host access were later widened.
+ * Default scripts see only data bindings: {@code body}, {@code header}/{@code 
headers},
+ * {@code exchangeProperty}/{@code exchangeProperties}, {@code 
variable}/{@code variables} and {@code exchangeId}. The
+ * host objects ({@code exchange}, {@code camelContext}, {@code message}, 
{@code request} and {@code exception}) are
+ * intentionally absent so they resolve as Python {@code NameError} rather 
than opaque objects with no usable API.
+ * Binding them by default would also become a privilege escalation if host 
access were later widened.
  * </p>
  *
  * <p>
@@ -72,8 +75,8 @@ public class Python3Language extends TypedLanguageSupport 
implements ScriptingLa
 
     private final HostAccess hostAccess;
     /**
-     * When true, also bind {@code exchange}, {@code message}, and {@code 
context}. Only {@link #createWithHostAccess()}
-     * sets this; default mode keeps those names undefined.
+     * When true, also bind the host objects {@code exchange}, {@code 
camelContext}, {@code message}, {@code request}
+     * and {@code exception}. Only {@link #createWithHostAccess()} sets this; 
default mode keeps those names undefined.
      */
     private final boolean bindCamelHostObjects;
     private final Map<String, Source> sourceCache = 
LRUCacheFactory.newLRUSoftCache(16, 1000, true);
@@ -95,8 +98,8 @@ public class Python3Language extends TypedLanguageSupport 
implements ScriptingLa
 
     /**
      * Creates a separate language instance for trusted scripts. Uses {@link 
HostAccess#ALL} so Python may call public
-     * methods and fields on bound host objects, and additionally exposes 
{@code exchange}, {@code message}, and
-     * {@code context}.
+     * methods and fields on bound host objects, and additionally exposes 
{@code exchange}, {@code camelContext},
+     * {@code message}, {@code request} and {@code exception}.
      * <p>
      * This is an explicit opt-in: {@code HostAccess.ALL} is not a sandbox. It 
does not enable {@code allowAllAccess},
      * Java class lookup, host IO, or process creation. Use only when you 
trust the scripts.
@@ -166,16 +169,26 @@ public class Python3Language extends TypedLanguageSupport 
implements ScriptingLa
     Object evaluateExpression(String script, Exchange exchange) {
         try (Context cx = Python3Helper.newContext(engine(), hostAccess)) {
             Value b = cx.getBindings("python");
-            // Default: data only. Do not bind exchange/message/context — they 
are undefined (NameError)
-            // unless createWithHostAccess() opted into trusted host-object 
bindings.
+            Message message = exchange.getMessage();
+            Map<String, Object> headers = message.getHeaders();
+            Map<String, Object> properties = exchange.getAllProperties();
+            Map<String, Object> variables = exchange.getVariables();
+            // Default: data only, with the data names 
ExchangeHelper.populateVariableMap gives Groovy. The host objects
+            // are undefined (NameError) unless createWithHostAccess() opted 
into trusted host-object bindings.
             b.putMember("exchangeId", exchange.getExchangeId());
-            b.putMember("headers", exchange.getMessage().getHeaders());
-            b.putMember("properties", exchange.getAllProperties());
-            b.putMember("body", exchange.getMessage().getBody());
+            b.putMember("body", message.getBody());
+            b.putMember("header", headers);
+            b.putMember("headers", headers);
+            b.putMember("exchangeProperty", properties);
+            b.putMember("exchangeProperties", properties);
+            b.putMember("variable", variables);
+            b.putMember("variables", variables);
             if (bindCamelHostObjects) {
                 b.putMember("exchange", exchange);
-                b.putMember("message", exchange.getMessage());
-                b.putMember("context", exchange.getContext());
+                b.putMember("camelContext", exchange.getContext());
+                b.putMember("message", message);
+                b.putMember("request", message);
+                b.putMember("exception", LanguageHelper.exception(exchange));
             }
             Value value = cx.eval(source(script));
             return convert(value, Object.class, exchange.getContext(), 
exchange);
diff --git 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.java
 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.java
new file mode 100644
index 000000000000..16450b322b27
--- /dev/null
+++ 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.python3;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static 
org.apache.camel.language.python3.Python3LanguageSecurityTest.assertNameError;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The script variables have the names {@code 
ExchangeHelper.populateVariableMap} gives Groovy: default mode binds only
+ * the data names, trusted host access also the host objects. python3 is new, 
so the deprecated {@code properties} and
+ * {@code context} of the js and python languages are not bound in either mode.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class Python3BindingNamesTest extends CamelTestSupport {
+
+    private Exchange sampleExchange() {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody("hello");
+        exchange.getIn().setHeader("foo", "abc");
+        exchange.setProperty("color", "red");
+        exchange.setVariable("name", "Camel");
+        return exchange;
+    }
+
+    @Test
+    void defaultModeBindsGroovyDataNames() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = sampleExchange();
+
+        assertThat(evaluate(language, exchange, "body")).isEqualTo("hello");
+        assertThat(evaluate(language, exchange, 
"header['foo']")).isEqualTo("abc");
+        assertThat(evaluate(language, exchange, 
"headers['foo']")).isEqualTo("abc");
+        assertThat(evaluate(language, exchange, 
"exchangeProperty['color']")).isEqualTo("red");
+        assertThat(evaluate(language, exchange, 
"exchangeProperties['color']")).isEqualTo("red");
+        assertThat(evaluate(language, exchange, 
"variable['name']")).isEqualTo("Camel");
+        assertThat(evaluate(language, exchange, 
"variables['name']")).isEqualTo("Camel");
+        assertThat(evaluate(language, exchange, 
"exchangeId")).isEqualTo(exchange.getExchangeId());
+    }
+
+    @Test
+    void defaultModeDoesNotBindDeprecatedNames() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = sampleExchange();
+
+        assertNameError(language, exchange, "properties");
+        assertNameError(language, exchange, "context");
+    }
+
+    @Test
+    void defaultModeDoesNotBindHostObjects() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = sampleExchange();
+        exchange.setException(new IllegalArgumentException("Forced"));
+
+        assertNameError(language, exchange, "exchange");
+        assertNameError(language, exchange, "camelContext");
+        assertNameError(language, exchange, "message");
+        assertNameError(language, exchange, "request");
+        assertNameError(language, exchange, "exception");
+        assertNameError(language, exchange, "context");
+    }
+
+    @Test
+    void trustedModeBindsGroovyNames() {
+        Python3Language trusted = Python3Language.createWithHostAccess();
+        trusted.setCamelContext(context);
+        trusted.start();
+        try {
+            Exchange exchange = sampleExchange();
+
+            assertThat(evaluate(trusted, exchange, 
"exchange.getExchangeId()")).isEqualTo(exchange.getExchangeId());
+            assertThat(evaluate(trusted, exchange, 
"camelContext.getName()")).isEqualTo(context.getName());
+            assertThat(evaluate(trusted, exchange, 
"message.getHeader('foo')")).isEqualTo("abc");
+            assertThat(evaluate(trusted, exchange, 
"request.getHeader('foo')")).isEqualTo("abc");
+            assertThat(evaluate(trusted, exchange, 
"header['foo']")).isEqualTo("abc");
+            assertThat(evaluate(trusted, exchange, 
"exchangeProperties['color']")).isEqualTo("red");
+            assertThat(evaluate(trusted, exchange, 
"variables['name']")).isEqualTo("Camel");
+            assertThat(trusted.createPredicate("exception is 
None").matches(exchange)).isTrue();
+
+            exchange.setException(new IllegalArgumentException("Forced"));
+            assertThat(evaluate(trusted, exchange, 
"exception.getMessage()")).isEqualTo("Forced");
+        } finally {
+            trusted.stop();
+        }
+    }
+
+    @Test
+    void trustedModeDoesNotBindDeprecatedNames() {
+        Python3Language trusted = Python3Language.createWithHostAccess();
+        trusted.setCamelContext(context);
+        trusted.start();
+        try {
+            Exchange exchange = sampleExchange();
+
+            assertNameError(trusted, exchange, "context");
+            assertNameError(trusted, exchange, "properties");
+        } finally {
+            trusted.stop();
+        }
+    }
+
+    private static String evaluate(Language language, Exchange exchange, 
String script) {
+        return language.createExpression(script).evaluate(exchange, 
String.class);
+    }
+}
diff --git 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
index 5d39717e2f09..1448fe9c0b12 100644
--- 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
+++ 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
@@ -92,7 +92,7 @@ class Python3LanguageEvalTest {
         Language language = context.resolveLanguage("python3");
         assertThat(language.createExpression("2 + body").evaluate(exchange, 
Integer.class)).isEqualTo(9);
         
assertThat(language.createExpression("headers['foo']").evaluate(exchange, 
String.class)).isEqualTo("bar");
-        
assertThat(language.createExpression("properties['color']").evaluate(exchange, 
String.class)).isEqualTo("red");
+        
assertThat(language.createExpression("exchangeProperties['color']").evaluate(exchange,
 String.class)).isEqualTo("red");
         assertThat(language.createExpression("exchangeId").evaluate(exchange, 
String.class))
                 .isEqualTo(exchange.getExchangeId());
         assertThat(language.createPredicate("body == 
7").matches(exchange)).isTrue();
diff --git 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
index 1c6550c6134e..1bac95904a7e 100644
--- 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
+++ 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
@@ -76,7 +76,7 @@ class Python3LanguageSecurityTest {
         assertThat(language.createExpression("headers['written'] = 
'yes'\nheaders['written']").evaluate(exchange,
                 String.class)).isEqualTo("yes");
         assertThat(exchange.getIn().getHeader("written")).isEqualTo("yes");
-        
assertThat(language.createExpression("properties['color']").evaluate(exchange, 
String.class)).isEqualTo("red");
+        
assertThat(language.createExpression("exchangeProperties['color']").evaluate(exchange,
 String.class)).isEqualTo("red");
         assertThat(language.createExpression("exchangeId").evaluate(exchange, 
String.class))
                 .isEqualTo(exchange.getExchangeId());
     }
@@ -121,7 +121,7 @@ class Python3LanguageSecurityTest {
                 .isEqualTo(exchange.getExchangeId());
         
assertThat(trusted.createExpression("message.getBody()").evaluate(exchange, 
Object.class))
                 .isInstanceOf(Person.class);
-        
assertThat(trusted.createExpression("context.getName()").evaluate(exchange, 
String.class))
+        
assertThat(trusted.createExpression("camelContext.getName()").evaluate(exchange,
 String.class))
                 .isEqualTo(context.getName());
         
assertThat(trusted.createExpression("body.getAge()").evaluate(exchange, 
Integer.class)).isEqualTo(36);
         assertThat(trusted.createExpression("body.name").evaluate(exchange, 
String.class)).isEqualTo("Ada");
diff --git 
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
new file mode 100644
index 000000000000..a7f77dfd1ced
--- /dev/null
+++ 
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.python3;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static 
org.apache.camel.language.python3.Python3LanguageSecurityTest.assertDenied;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exchange variables are bound as the data binding {@code variables}, in the 
same way as {@code headers}: default mode
+ * may index the map but may not call Java methods on it.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class Python3VariablesTest extends CamelTestSupport {
+
+    @Test
+    void defaultModeBindsVariablesAsData() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        
assertThat(language.createExpression("variables['foo']").evaluate(exchange, 
String.class)).isEqualTo("bar");
+        assertThat(language.createExpression("variables['num'] + 
2").evaluate(exchange, Integer.class)).isEqualTo(7);
+        assertThat(language.createExpression("'missing' in 
variables").evaluate(exchange, Boolean.class)).isFalse();
+        assertThat(language.createPredicate("variables['foo'] == 
'bar'").matches(exchange)).isTrue();
+        assertThat(language.createPredicate("variables['foo'] == 
'baz'").matches(exchange)).isFalse();
+    }
+
+    @Test
+    void defaultModeWritesPropagateLikeHeaders() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setVariable("foo", "bar");
+
+        assertThat(language.createExpression("variables['written'] = 
'yes'\nvariables['written']")
+                .evaluate(exchange, String.class)).isEqualTo("yes");
+        assertThat(exchange.getVariable("written")).isEqualTo("yes");
+    }
+
+    @Test
+    void defaultModeDeniesJavaMethodsOnVariables() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setVariable("foo", "bar");
+
+        assertDenied(language, exchange, "variables.put('k', 'v')", 
"AttributeError");
+        assertDenied(language, exchange, "variables.getClass()", 
"AttributeError");
+    }
+
+    @Test
+    void trustedModeAlsoBindsVariables() {
+        Python3Language trusted = Python3Language.createWithHostAccess();
+        trusted.setCamelContext(context);
+        trusted.start();
+        try {
+            Exchange exchange = new DefaultExchange(context);
+            exchange.setVariable("foo", "bar");
+
+            
assertThat(trusted.createExpression("variables['foo']").evaluate(exchange, 
String.class)).isEqualTo("bar");
+            
assertThat(trusted.createExpression("exchange.getVariable('foo')").evaluate(exchange,
 String.class))
+                    .isEqualTo("bar");
+        } finally {
+            trusted.stop();
+        }
+    }
+
+    @Test
+    void variablesAreBoundForIndirectLookup() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setVariable("foo", "bar");
+
+        assertThat(language.createExpression("globals()['vari' + 
'ables']['foo']").evaluate(exchange, String.class))
+                .isEqualTo("bar");
+    }
+
+    @Test
+    void variablesAreBoundWhenNoVariableIsSet() {
+        Language language = context.resolveLanguage("python3");
+        Exchange exchange = new DefaultExchange(context);
+
+        assertThat(language.createPredicate("'foo' in globals()['vari' + 
'ables']").matches(exchange)).isFalse();
+    }
+
+    @Test
+    void variablesSetInRoute() throws Exception {
+        getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+        getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader("direct:start", "Camel", "greeting", 
"Hello");
+        template.sendBodyAndHeader("direct:start", "World", "greeting", 
"Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setVariable("name", body())
+                        .setVariable("greeting", header("greeting"))
+                        .setBody().python3("f\"{variables['greeting']} 
{variables['name']}\"")
+                        .choice()
+                        .when().python3("variables['name'] == 
'Camel'").to("mock:camel")
+                        .otherwise().to("mock:other");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
new file mode 100644
index 000000000000..2a5629ea914f
--- /dev/null
+++ 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.spel;
+
+import org.apache.camel.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables}, 
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code 
getHeader(...)}.
+ */
+class SpelVariablesTest extends LanguageTestSupport {
+
+    @Test
+    void variablesAreBound() {
+        exchange.setVariable("foo", "bar");
+        exchange.setVariable("num", 5);
+
+        assertExpression("#{variables.foo}", "bar");
+        assertExpression("#{variables['foo']}", "bar");
+        assertExpression("#{getVariable('foo')}", "bar");
+        assertExpression("#{variables.num + 2}", 7);
+        assertExpression("#{getVariable('missing')}", null);
+        assertPredicate("#{variables.foo == 'bar'}");
+        assertPredicate("#{getVariable('foo') == 'baz'}", false);
+    }
+
+    @Test
+    void typedVariableGetterConverts() {
+        exchange.setVariable("num", "123");
+
+        assertExpression("#{getVariable('num', T(Integer)) + 1}", 124);
+        assertPredicate("#{getVariable('num', T(Integer)) > 100}");
+    }
+
+    @Test
+    void globalVariableThroughGetter() {
+        exchange.setVariable("global:greeting", "Hi");
+
+        assertExpression("#{getVariable('global:greeting')}", "Hi");
+    }
+
+    @Override
+    protected String getLanguageName() {
+        return "spel";
+    }
+}
diff --git 
a/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc 
b/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
index b1b1e41c577f..76c0c583257f 100644
--- 
a/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
+++ 
b/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
@@ -40,13 +40,21 @@ The following Camel related variables are made available:
 |request |Message |the message
 |message |Message |the message
 |headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given 
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the 
given type
 |properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the 
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the 
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the 
given type
 |=======================================================================
 
+`getHeader`, `getProperty` and `getVariable` are methods on the root object, 
so they are
+called by their full name, as in `#{getHeader('foo')}` or 
`#{getVariable('foo')}`. The maps can be
+read as `#{headers.foo}` or `#{variables['foo']}`. `getVariable` also resolves 
global and route
+variables, such as `#{getVariable('global:foo')}`, which are not in the 
`variables` map.
+
 == Example
 
 You can use SpEL as an expression for 
xref:eips:recipientList-eip.adoc[Recipient
diff --git 
a/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
 
b/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
index 8f6c4ce7296e..1bb18d8e9bff 100644
--- 
a/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
+++ 
b/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
@@ -86,4 +86,29 @@ public final class RootObject {
         return exchange.getMessage().getHeader(name, type);
     }
 
+    /**
+     * The variables of the exchange (exchange-scoped only; global and route 
variables are not in this map).
+     *
+     * @see #getVariable(String) to look up a global or route variable by its 
prefixed name
+     */
+    public Map<String, Object> getVariables() {
+        return exchange.getVariables();
+    }
+
+    /**
+     * The variable with the given name, which can be prefixed with {@code 
global:} or {@code route:} for a global or
+     * route variable.
+     */
+    public Object getVariable(String name) {
+        return exchange.getVariable(name);
+    }
+
+    /**
+     * The variable with the given name converted to the given type, which can 
be prefixed with {@code global:} or
+     * {@code route:} for a global or route variable.
+     */
+    public <T> T getVariable(String name, Class<T> type) {
+        return exchange.getVariable(name, type);
+    }
+
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 34c6c60689ce..ae3b3396c258 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2011,6 +2011,14 @@ polyglot `Value` itself needs to work with these types 
instead.
 
 `JavaScriptHelper.newContext()` is deprecated: contexts are created by the 
language from its shared engine.
 
+=== camel-javascript, camel-python - script variables use the Groovy names
+
+The `js` and `python` languages now bind the script variables with the names 
the `groovy` language uses:
+`camelContext`, `request`, `header`, `exchangeProperty`, `exchangeProperties` 
and `exception` are new, next to
+`exchange`, `exchangeId`, `message`, `body`, `headers`, `variable` and 
`variables`. In `js` and `python` the
+`context` and `properties` variables are deprecated in favour of 
`camelContext` and `exchangeProperties`; they still
+work. The new `python3` language binds only the Groovy names, and in its 
default data-only mode only the data variables.
+
 === camel-rest, camel-rest-openapi, camel-jbang - camel-http is the default 
REST client
 
 When a REST producer (`rest:` in producer mode, or `rest-openapi` calling an 
operation of a contract) has no

Reply via email to