Ports (Experimental)

Note

The ports module (py_trees.ports) and the XML parser (py_trees.parsers.behaviour_tree_xml) are experimental. Their API may change between releases.

Overview

Ports add a structured way of wiring up data exchange between nodes. Each node defines the data it reads and writes as input and output ports. The ports are wired to blackboard keys (the remapping), and port values are type-checked at runtime. This allows for a defined and constrained way of data exchange between nodes which is less error-prone and easier to debug than simply writing data on the blackboard and letting nodes read from and write to that entry directly.

Why use ports?

Using ports instead of ad-hoc blackboard reads and writes pays off in several concrete ways:

  • Explicit data contracts. A node’s input_ports() and output_ports() declarations are its data-flow API. A reader can see at a glance what a node consumes and produces without reading through its update() method.

  • Structured, early error detection. Port values are type-checked at runtime (TypeError on mismatched writes/reads), required inputs without data raise NoDataAvailable instead of returning None silently, and misconfiguration (a remap for a port that isn’t declared, or a port type that can’t accept the wiring) surfaces at setup time rather than deep in a tick.

  • XML authoring. Once a library of port-enabled nodes exists, trees become declarative data. Non-programmers can read and edit tree structure (and the data wiring) without touching Python. See the XML parser section below.

  • Reusable subtrees via rewiring. A subtree is configured from the outside by rewiring its port remappings. A subtree can be re-used with different inputs and outputs without touching the internal code. In combination with the XML parser, this concept of re-usable subtrees becomes really powerful to quickly put together new behaviors.

  • Automatic subtree isolation. Sibling subtrees can freely reuse the same port names internally; the subtree namespace scopes them on the blackboard so they don’t collide.

  • Refactoring safety. Changing the blackboard key a data item lives under is a rewiring change at setup time, not a code change scattered across every node that used to read or write that key by name.

  • Easier to test. A single node can be exercised in isolation by wiring its ports to known blackboard keys, seeding the inputs, and ticking once. No tree scaffolding required; the ports contract becomes the test surface.

The primary API is py_trees.ports.PortsMixin. Concrete nodes typically inherit from the convenience base py_trees.ports.BehaviourWithPorts, which combines the mixin with py_trees.behaviour.Behaviour:

import py_trees
from py_trees.ports import BehaviourWithPorts, PortInformation

class Multiply(BehaviourWithPorts):
    @classmethod
    def input_ports(cls):
        return {
            "a": PortInformation(data_type=float, required=True),
            "b": PortInformation(data_type=float, required=True),
        }

    @classmethod
    def output_ports(cls):
        return {
            "product": PortInformation(data_type=float, required=True),
        }

    def update(self):
        self._set_output("product", self.get_input("a") * self.get_input("b"))
        return py_trees.common.Status.SUCCESS

Wiring (remapping) and type checking

Ports become usable after setup_ports() has been called with the port remappings (the “wiring”). Remappings map each port to an absolute or relative blackboard key. The purpose of remapping is to “wire” one node’s output port(s) to another node’s input port(s) so they can exchange data.

node = Multiply(name="mul")
node.setup_ports(
    port_remappings={
        "a":       "/numbers/a",
        "b":       "/numbers/b",
        "product": "/numbers/product",
    }
)

In the example above, another node’s output ports would typically be remapped to /numbers/a and /numbers/b and thereby provide the input for the Multiply node.

Note

Why is setup_ports() a separate call? Because the remapping table usually cannot be computed until the entire tree topology is known — either the user assembles it by hand or a parser generates it from e.g. XML (more on that next). See py_trees.ports.PortsMixin for the full contract and semantics.

Experimental XML parser

The module py_trees.parsers.behaviour_tree_xml ships an (experimental) parser for the BehaviorTree.CPP XML format. It builds a py_trees tree from an XML file and auto-generates the port remappings for every node.

from py_trees.parsers.behaviour_tree_xml import parse_behaviour_tree_xml

root = parse_behaviour_tree_xml("my_tree.xml")

How the parser maps each XML tag to a Python class is covered in Resolving node classes just below. The demos further down show complete working examples.

Resolving node classes

An XML tag like <MyNode> is just a name — before the parser can build the node it has to find the Python class that name refers to. This section explains how that lookup happens and how you can customize it.

There is a node_registry argument which controls that lookup, and takes one of two forms.

1. Auto (the default). Every concrete PortsMixin subclass registers itself (under its class name) the moment it is defined. So as long as your node classes have been imported, the parser resolves every tag with no extra configuration — node_registry="auto" is the default, so you pass nothing:

from py_trees.parsers.behaviour_tree_xml import parse_behaviour_tree_xml
from my_nodes import MyNode, OtherNode  # importing is enough to register them

root = parse_behaviour_tree_xml("my_tree.xml")

2. An explicit dict. Pass a {tag: class-or-callable} mapping to use exactly that mapping and ignore the auto-registry. This is the form to reach for when you need to inject a dependency, alias a tag, or sandbox the parser to a fixed set of classes:

root = parse_behaviour_tree_xml(
    "my_tree.xml",
    node_registry={"MyNode": MyNode, "OtherNode": OtherNode},  # only these, no auto
)

To combine auto-registration with a few explicit entries — most commonly to inject a runtime dependency via functools.partial (a partial isn’t a class, so it can’t live in the auto-registry) — spread the auto-registry into your dict and override the entries you need:

import py_trees
from functools import partial

root = parse_behaviour_tree_xml(
    "my_tree.xml",
    node_registry={
        **py_trees.ports.get_ports_registry(),       # everything auto-registered, plus...
        "Wait": partial(Wait, factory=my_factory),   # ...a dependency-injected override
    },
)

Aliasing. To expose a class under an extra tag, register it under that tag — either inline on the class definition or via the helper for classes you cannot modify. Either way it then resolves under the alias in the "auto" path:

class Foo(PortsMixin, py_trees.behaviour.Behaviour, tag="Bar"):
    ...

# or, for a third-party class:
from py_trees.ports import register_ports_class
register_ports_class("Bar", Foo)

Pass register=False on a class definition to keep a particular subclass out of the registry. Still-abstract classes (e.g. BehaviourWithPorts itself, which does not implement input_ports / output_ports) are never registered.

XML attributes: ports and constructor arguments

Attributes on a node’s XML tag serve two distinct purposes:

  1. Attribute names that match a declared port (input_ports() or output_ports()) are treated as port remappings. Values may be {curly_key} references (wired to the remapping table) or literal constants (type-converted according to the port’s declared type).

  2. Attribute names that do not match any declared port are treated as constructor keyword arguments and forwarded to the class constructor. Values are type-converted based on the constructor’s type annotations (strings to int / float / bool / enum / etc.); un-annotated parameters receive the raw string. {curly_key} references are not allowed for constructor kwargs — they raise a ValueError at parse time, because constructor kwargs can’t be re-wired at runtime the way ports can.

Example:

class Greeting(BehaviourWithPorts):

    @classmethod
    def input_ports(cls):
        return {"name_key": PortInformation(data_type=str, required=True)}

    @classmethod
    def output_ports(cls):
        return {"greeting": PortInformation(data_type=str, required=True)}

    def __init__(self, name: str, prefix: str = "Hello", **kwargs):
        super().__init__(name=name, **kwargs)
        self.prefix = prefix        # not a port; populated from XML attribute

    def update(self):
        self._set_output("greeting", f"{self.prefix}, {self.get_input('name_key')}!")
        return py_trees.common.Status.SUCCESS
<Greeting name="hello_node" name_key="{target}" prefix="Howdy"/>
<!--       ^^^ behaviour name   ^^^ port remap    ^^^ ctor kwarg  -->

Composing trees from multiple files

Subtrees do not have to live in the same file as the tree that uses them. A top-level <Include> (or <Import>) element pulls all <BehaviorTree> definitions from another XML file into the current document before parsing, so you can maintain a library of reusable subtrees in separate files:

<!-- subtree_library.xml -->
<root>
  <BehaviorTree ID="LibraryTree">
    <Sequence>
      <Producer name="LibProducer" output="{lib_out}"/>
    </Sequence>
  </BehaviorTree>
</root>
<!-- main_tree.xml -->
<root main_tree_to_execute="MainTree">
  <Include src="subtree_library.xml"/>
  <BehaviorTree ID="MainTree">
    <Sequence>
      <SubTree ID="LibraryTree" name="Library" lib_out="{final}"/>
      <Consumer name="FinalConsumer" input="{final}"/>
    </Sequence>
  </BehaviorTree>
</root>

The path in src= (file= is accepted as an alias) is resolved in order:

  1. As an absolute path, if it is one.

  2. Relative to the directory of the including XML file.

  3. Against each directory in the search_paths argument of parse_behaviour_tree_xml().

So if the library file lives somewhere other than next to the main file — say a shared subtrees/ directory — pass that directory via search_paths:

root = parse_behaviour_tree_xml(
    "main_tree.xml",
    search_paths=["/path/to/subtrees"],
)

A few more rules to be aware of:

  • Includes are only recognised as top-level children of <root>; an <Include> nested inside a <BehaviorTree> is an error.

  • Includes are processed recursively — an included file may itself include further files — and cycles are detected and skipped.

  • All BehaviorTree IDs must be unique across the main file and everything it includes; a collision raises ValueError.

  • main_tree_to_execute may refer to a tree defined in an included file.

Scope, limitations, and how to extend

The current ports framework is deliberately minimal. It ships the PortsMixin contract, the convenience BehaviourWithPorts base, an experimental XML parser, and four demos. It is the base for extensions to be added in future. A few things you should be aware of, and suggestions on how to fill the gaps yourself:

1. No port-aware behaviours, decorators, or composites are shipped.

py_trees.ports provides the mechanism for typed input/output ports. It does not currently ship any concrete behaviours that use ports (e.g. there is no Retry with ports). The library of port-enabled nodes is the user’s domain: you define PortsMixin-derived classes that actually do something with the input/output data.

2. Built-in decorators and composites cannot be wired through ports from XML.

The XML parser supports four built-in composite tags natively (<Sequence>, <Selector> / <Fallback>, <Parallel>). For these tags, only a fixed set of XML attributes is consumed:

  • <Sequence> / <Selector> / <Fallback>: name, memory

  • <Parallel>: name, policy (one of success_on_all, success_on_one, success_on_selected)

Any other attribute on a built-in composite tag is silently ignored. For example, <Parallel synchronise="true"> has no effect, and port-style attributes on these tags are dropped without warning. This is also why you cannot take the number of attempts for a py_trees.decorators.Retry from a port value via XML — the parser only wires ports (and forwards constructor kwargs; see XML attributes: ports and constructor arguments above) on resolved classes that derive from PortsMixin (whether resolved via the "auto" registry or an explicit node_registry dict; see Resolving node classes above). Built-in decorators aren’t recognised as XML tags at all.

3. The pattern: port-aware wrapper classes.

To make an existing upstream behaviour, decorator, or composite port-aware, wrap it in a small adapter class that combines PortsMixin with the upstream class and reads its runtime parameters from input ports.

The recommended naming is to keep the short upstream class name (Retry, Repeat, Parallel, …) and place the port-aware version under a ports submodule that mirrors the upstream layout (e.g. py_trees.ports.decorators.Retry alongside the upstream py_trees.decorators.Retry). The import path carries the “ported” information, so the class name stays short and matches its upstream counterpart.

Example: a port-aware Retry that takes its num_failures from an input port — intended to live in py_trees.ports.decorators when contributed upstream, or in your own project’s ports submodule:

# py_trees/ports/decorators.py  (or yourproject/ports/decorators.py)
import py_trees
from py_trees.ports import PortInformation, PortsMixin

class Retry(PortsMixin, py_trees.decorators.Retry):
    """Retry that reads its failure budget from an input port."""

    @classmethod
    def input_ports(cls):
        return {"num_failures": PortInformation(data_type=int, required=True)}

    @classmethod
    def output_ports(cls):
        return {}

    def __init__(
        self,
        name: str,
        child: py_trees.behaviour.Behaviour,
        **kwargs,
    ):
        # Start with a safe default; it is overwritten on every initialise().
        super().__init__(
            name=name, child=child, num_failures=1, **kwargs
        )

    def initialise(self) -> None:
        # Read the port value at tick boundary and apply it before the
        # upstream Retry logic runs.
        self.num_failures = self.get_input("num_failures")
        super().initialise()

Users import it as from py_trees.ports.decorators import Retry (or the equivalent path in their own project) — the import path disambiguates it from the upstream py_trees.decorators.Retry, so the class name stays clean.

The XML then can accept the input via (remapped) ports:

<Retry name="retry" num_failures="{retry_budget}">
    <SomeBehaviour name="worker" />
</Retry>

Note

Contributing port-enabled extensions upstream is encouraged!

If you build a generally useful port-aware wrapper — for example, a port-enabled Retry, Repeat, Parallel, or Timer — please consider contributing it back to py_trees under the matching py_trees.ports.* subpackage (py_trees.ports.decorators, py_trees.ports.composites, py_trees.ports.timers, and so on, mirroring the upstream module layout). A shared library of canonical port-aware adapters saves every user from re-implementing the same wrappers. Open a PR against the py_trees devel branch and we will happily review it.

Demos

Example programs live in py_trees.demos.ports. Each demo module has a top-level docstring describing the scenario it demonstrates.

Module reference

Typed input/output ports for py_trees behaviours.

class py_trees.ports.BehaviourWithPorts(name: str, **kwargs: Any)[source]

Bases: PortsMixin, Behaviour

Base class for behaviours with typed input and output ports (see PortsMixin).

Subclassing requirements:

  • Each subclass must implement the input_ports and output_ports class methods to specify its input and output ports.

  • Each subclass must implement the update() method to define its behaviour.

  • Other methods from py_trees.behaviour.Behaviour may be overridden as needed.

Example usage:

class ExampleBehaviour(BehaviourWithPorts):
    @classmethod
    def input_ports(cls):
        return {"input_data": PortInformation(data_type=str, required=True)}

    @classmethod
    def output_ports(cls):
        return {"output_data": PortInformation(data_type=str, required=True)}

    def update(self):
        # Implementation of the behaviour
        ...

Status semantics for update():

  • Returning FAILURE indicates a technical error (e.g. service call failures, exceptions) that may potentially be handled by nodes such as Retry.

  • If not resolved, a FAILURE will cause the entire tree to fail.

  • Do not use FAILURE as an expected logical outcome, just so that nodes like Retry can handle those (e.g. when no objects are detected in an image — that is a valid result, not a failure). Instead, indicate such logical outcomes via node outputs (e.g. an empty list of detected objects).

Parameters:
  • name (str) –

  • kwargs (Any) –

update() Status[source]

Subclass-defined tick logic. Must return a Status.

Return type:

Status

exception py_trees.ports.NoDataAvailable[source]

Bases: Exception

Exception raised when a required data has not (yet) been written to the port.

class py_trees.ports.PortInformation(data_type: Any, required: bool = True, description: str = '')[source]

Bases: object

Static declaration for one typed input or output port.

Parameters:
  • data_type (Any) –

  • required (bool) –

  • description (str) –

class py_trees.ports.PortsMixin(*args: Any, behaviour_class_name: str | None = None, **kwargs: Any)[source]

Bases: ABC

Mixin class for enabling input and output ports on behaviour tree nodes.

This mixin provides the core infrastructure needed to wire, validate, and execute data-driven behaviour tree nodes in a modular and reusable way. It is designed to be used as a base class for behaviours, composites, and decorators in behaviour trees.

A class using PortsMixin must:

  1. Inherit from PortsMixin first, followed by a concrete py_trees class (e.g. py_trees.behaviour.Behaviour).

  2. Define its input and output ports as class-level information by implementing the @classmethod s input_ports and output_ports.

A PortsMixin represents a modular unit that interacts with input and output data through well-defined ports. These ports are typed and validated at runtime to ensure consistency and facilitate composability between different nodes.

Subclasses must define their input and output ports as class-level information by implementing the @classmethod s input_ports and output_ports.

  • input_ports(cls): returns a dictionary mapping input port names to port information.

  • output_ports(cls): returns a dictionary mapping output port names to port information.

These methods return the expected port definitions for the class and do not change at runtime. These port definitions are used to:

  1. Register blackboard keys for communication.

  2. Enforce type and presence validation at runtime.

  3. Provide clear contracts for each behaviour’s data dependencies and outputs.

Example usage:

class MyBehaviour(PortsMixin, py_trees.behaviour.Behaviour):
    @classmethod
    def input_ports(cls):
        return {"input": PortInformation(data_type=str, required=True)}

    @classmethod
    def output_ports(cls):
        return {"output": PortInformation(data_type=str, required=True)}

    def __init__(self, name: str):
        super().__init__(name=name)

    def update(self):
        input_val = self.get_input("input")
        self._set_output("output", f"Processed({input_val})")
        return py_trees.common.Status.SUCCESS

Port specification format in input_ports() and output_ports():

{
    "<port_name>": PortInformation(data_type=<expected_type>, required=<bool>),
}

Input and output port names must be unique across both sets; overlapping names are not allowed and will raise a ValueError at instantiation.

Subtrees

PortsMixin is designed to be used in complex behaviour trees that may consist of multiple subtrees. Each behaviour operates within a specified subtree_namespace, allowing multiple instances of the same behaviour to run in parallel without interfering with each other’s blackboard keys. The namespace ensures logical separation between behaviours and enables modular composition of behaviour trees.

Blackboard access

PortsMixin operates within a scoped subtree namespace to ensure its blackboard keys don’t clash when used in multiple subtrees. Always use the blackboard property, as it provides access to the correctly namespaced client. The class abstracts away direct blackboard access in favour of get_input() and _set_output() methods. Direct access to the blackboard is discouraged and only permitted through the blackboard property after setup_ports() has been called. However, be aware that the blackboard is shared with all other behaviours in the same subtree, so care must be taken to avoid key collisions.

Blackboard namespace strategy

When a port is not explicitly remapped (via a remapping table or constructor arguments), a dedicated storage key is generated, so that sibling nodes with the same port name do not accidentally share data. This “synthesised” key is derived from:

  • the current subtree namespace,

  • the node’s name (sanitised to remove characters that py_trees treats as separators),

  • and the node’s UUID.

Behaviours can continue to share ports by wiring the same absolute key on purpose (e.g. /shared/output).

Port remapping

During setup, ports may be remapped to alternate blackboard keys using the port_remappings argument.

Port setup lifecycle

setup_ports() is a separate explicit call rather than part of Behaviour.setup() because port setup requires the full remapping table, which may require parsing the entire tree to compute. The remapping is the “wiring” of ports — connecting one node’s inputs to another node’s outputs — so it presupposes knowledge of the tree topology.

Output write semantics

Output ports are written internally by the node itself (typically inside update()). External callers should not write to output ports in production code; writing from outside is only expected in unit tests where the blackboard is seeded manually.

Composites / decorators scope

PortsMixin can be mixed into any Behaviour subclass — leaves, composites, and decorators. However, this migration does not ship ports-enabled composite or decorator implementations. Those can be contributed in separate follow-up PRs.

Example:

class ConsumerProducer(PortsMixin, py_trees.behaviour.Behaviour):
    @classmethod
    def input_ports(cls):
        return {"input": PortInformation(data_type=str, required=True)}

    @classmethod
    def output_ports(cls):
        return {"output": PortInformation(data_type=str, required=True)}

    def update(self):
        input_val = self.get_input("input")
        self._set_output("output", f"Processed({input_val})")
        return py_trees.common.Status.SUCCESS
Parameters:
  • args (Any) –

  • behaviour_class_name (str | None) –

  • kwargs (Any) –

property behaviour_class_name: str

Return the name under which this behavior class is registered.

This is typically the class name (e.g., “CheckGraspStatus”), but can also be an alias used for partial instantiations or custom registrations in a behavior registry.

Returns:

The registered name of this behavior class.

Return type:

str

property blackboard_client: Client

Return the scoped blackboard client.

Raises:

RuntimeError – If setup_ports() has not been called before accessing the blackboard.

get_input(port_name: str, default: Any = None) Any[source]

Read the value of the given input port from the blackboard.

Parameters:
  • port_name (str) – The name of the input port to read from the blackboard.

  • default (Any) – Optional default value to return if the port has no input data.

  • None (If set to) –

  • accepted. (no default is) –

Returns:

The value retrieved from the blackboard for the specified input port or the default.

Return type:

Any

Raises:
  • KeyError – If the input port name is not defined.

  • TypeError – If the retrieved value does not match the expected type.

  • NoDataAvailable – If no data is available on the input port and no default is given.

get_last_output(port_name: str) Any[source]

Return the last output which the node wrote at this port.

Parameters:

port_name (str) – The name of the output port to read from the blackboard.

Returns:

The value retrieved from the blackboard for the specified output port.

Return type:

Any

Raises:
  • KeyError – If the input port name is not defined.

  • TypeError – If the value does not match the expected type.

  • NoDataAvailable – If no data has (yet) been written to the output port.

get_logger() PortsLogger[source]

Return the logger instance.

Raises:

RuntimeError – If setup_ports() has not been called before accessing the logger.

Return type:

PortsLogger

classmethod get_port_type(port_name: str) type[source]

Return the declared type for port_name.

Parameters:

port_name (str) – The name of the input or output port.

Returns:

The expected data type of the specified port.

Return type:

type

Raises:

KeyError – If the port name is not defined in either input or output ports.

abstract classmethod input_ports() dict[str, py_trees.ports.PortInformation][source]

Return a mapping of input port names to port information.

Return type:

dict[str, py_trees.ports.PortInformation]

classmethod is_port_required(port_name: str) bool[source]

Return whether the specified port is marked as required.

Parameters:

port_name (str) – The name of the input or output port.

Returns:

True if the specified port is marked as required, False otherwise.

Return type:

bool

Raises:

KeyError – If the port name is not defined in either input or output ports.

log(level: LogLevel, msg: str, return_only: bool = False, print_name: bool = True) str[source]

Log a message at the specified severity level and update feedback.

Parameters:
  • level (LogLevel) –

  • msg (str) –

  • return_only (bool) –

  • print_name (bool) –

Return type:

str

log_debug(msg: str, return_only: bool = False, print_name: bool = True) str[source]

Log msg at DEBUG level (see log()).

Parameters:
  • msg (str) –

  • return_only (bool) –

  • print_name (bool) –

Return type:

str

log_error(msg: str, return_only: bool = False, print_name: bool = True) str[source]

Log msg at ERROR level (see log()).

Parameters:
  • msg (str) –

  • return_only (bool) –

  • print_name (bool) –

Return type:

str

log_info(msg: str, return_only: bool = False, print_name: bool = True) str[source]

Log msg at INFO level (see log()).

Parameters:
  • msg (str) –

  • return_only (bool) –

  • print_name (bool) –

Return type:

str

log_warning(msg: str, return_only: bool = False, print_name: bool = True) str[source]

Log msg at WARNING level (see log()).

Parameters:
  • msg (str) –

  • return_only (bool) –

  • print_name (bool) –

Return type:

str

abstract classmethod output_ports() dict[str, py_trees.ports.PortInformation][source]

Return a mapping of output port names to port information.

Return type:

dict[str, py_trees.ports.PortInformation]

reset_all_output_ports() None[source]

Clear all output ports registered on this node.

Keeps the keys registered (READ/WRITE permissions unaffected), but removes the stored values. Intended for use-cases like “new data epoch” where downstream nodes should not read stale outputs.

This will have the effect that subsequent blackboard_client.exists(port_name) returns False again

Raises:

KeyError – if any port is unknown or not registered.

Return type:

None

reset_port(port_name: str) None[source]

Clear the value stored for a (usually output) port.

Keeps the key registered (READ/WRITE permissions unaffected), but removes the stored value. Intended for use-cases like “new data epoch” where downstream nodes should not read stale outputs.

This will have the effect that subsequent blackboard_client.exists(port_name) returns False again

Raises:

KeyError – if the port is unknown or not registered.

Parameters:

port_name (str) –

Return type:

None

setup_ports(port_remappings: dict | None = None, subtree_namespace: str = '/', logger: py_trees.ports_utils.PortsLogger | None = None) None[source]

Initialize the ports and prepare the blackboard interface.

Registers all declared input and output ports with the blackboard client, optionally applying custom key remappings, and sets the namespace and logger.

This method must be called before using get_input(), _set_output(), or accessing the blackboard or logger properties.

Port remapping rules

  1. If a port is not in port_remappings, its blackboard key is automatically constructed as /{subtree_namespace}/{port_name}.

  2. If a remapped key starts with /, it is treated as an absolute/global key.

  3. If a remapped key does not start with /, it is treated as a key relative to the given namespace, i.e. /{subtree_namespace}/{remapped_key}.

General notes

  1. The underlying data store is currently the py_trees.blackboard, but this is abstracted.

  2. However, the API of PortsMixin abstracts from the concept of a blackboard, so the underlying implementation could change later.

  3. Think of a “data key” as a generic handle to some shared data storage (like a key in a map), which can be remapped to match external system requirements.

Parameters:
  • port_remappings (dict) – Optional dictionary mapping port names to custom blackboard keys.

  • subtree_namespace (str) – Namespace to scope the blackboard client (default: "/").

  • logger (py_trees.ports_utils.PortsLogger | None) – Optional logger-like object with debug()/info()/warning()/error() methods. When None, falls back to self.logger (the native py_trees logger).

Raises:

KeyError – If a port in port_remappings is not declared in the port definitions.

Return type:

None

This function must be called before using get_input, _set_output, or accessing the blackboard property.

property subtree_namespace: str

Return the namespace associated with the subtree, used to scope blackboard keys.

Raises:

RuntimeError – If setup_ports() has not been called before accessing the namespace.

py_trees.ports.get_ports_registry() dict[str, type[py_trees.ports.PortsMixin]][source]

Return a shallow copy of the global ports registry ({tag: class}).

Return type:

dict[str, type[py_trees.ports.PortsMixin]]

py_trees.ports.register_ports_class(tag: str, cls: type) None[source]

Register cls under tag in the global ports registry.

Use this to alias a class under an additional tag, or to register a third-party PortsMixin subclass that cannot be modified to pass tag= / register= at definition time.

Parameters:
  • tag (str) – The tag (lookup name) to register under.

  • cls (type) – A concrete subclass of PortsMixin.

Raises:

TypeError – If cls is not a subclass of PortsMixin.

Return type:

None

XML parser for the BehaviorTree format.

Note

The parser is experimental and its API may change between releases.

This module provides a parser for the BehaviorTree XML format, used to construct behaviour trees with key remapping and subtree instantiation.

Overview

The parser recursively builds a behaviour tree from an XML file, using a remapping table to track key assignments and substitutions. The remapping table is a dictionary that maps keys (referenced in curly braces in the XML, e.g. {key}) to either their absolute paths (e.g. /some/key) or to other keys (e.g. {other_key}), which are then further resolved to absolute paths.

In the end, all keys in the tree map to absolute paths which can be used to address the value in a map, blackboard, or similar structure.

Remapping table example:

{
    "absolute_value": "/absolute/path",
    "curly_reference": "{absolute_value}",
}

Keys are resolved recursively until an absolute path is found. This allows flexible wiring of data flow between nodes and subtrees.

Subtree templates and instantiation

Subtrees are defined as <BehaviorTree ID="..."> elements in the XML. These act as templates, which can be instantiated elsewhere in the tree using a <SubTree> tag. When a subtree is instantiated, the parser:

  • Makes a local remapping table by applying any remappings specified in the <SubTree> tag.

  • Recursively parses the referenced subtree template, using the updated remapping table and a new namespace.

Example subtree template:

<BehaviorTree ID="MySubtree">
    <Sequence>
        <Reader name="MyReader" input="{input_key}" />
        <Writer name="MyInternalWriter" output="{transfer_key}" />
        <Reader name="MyInternalReader" input="{transfer_key}" />
    </Sequence>
</BehaviorTree>

Example main tree instantiating a subtree:

<BehaviorTree ID="MainTree">
    <Writer output="{some_key}" name="WriterMain" />
    <SubTree ID="MySubtree" name="Subtree1" input_key="{some_key}"/>
</BehaviorTree>

This example will map {some_key} to /some_key in the remapping table (the root namespace is /), and when the MySubtree is instantiated, it will create a new namespace /Subtree1 where:

  • {input_key} resolves to /some_key

  • {transfer_key} (which is not remapped in the SubTree tag) resolves to /Subtree1/transfer_key — so a new key is created for the subtree.

A good documentation of remapping and subtrees can be found on the BT.CPP documentation.

Parsing walkthrough

Given the above example, parsing proceeds as follows:

  1. The parser starts at the MainTree with an empty remapping table and namespace /.

  2. It encounters the Writer node, which uses {some_key}. Since this key is new, it is mapped to /some_key in the remapping table.

  3. The SubTree node is encountered. The parser:

    • Copies the current remapping table.

    • Adds input_key -> {some_key} to the remapping table for the subtree.

    • Sets the namespace to /Subtree1.

    • Recursively parses the MySubtree template.

  4. Inside MySubtree:

    • The Reader node uses {input_key}, which resolves (via remapping) to /some_key.

    • The Writer node uses {transfer_key}, which is new, so it is mapped to /Subtree1/transfer_key.

    • The second Reader node uses {transfer_key}, which now resolves to /Subtree1/transfer_key.

Key concepts

  • Remapping table: Tracks how keys in curly braces are resolved to absolute paths or to other keys.

  • Namespace: Each subtree instantiation gets its own namespace, ensuring keys are scoped and do not collide.

  • Subtree instantiation: Subtrees are templates; instantiating them is like copy-pasting their structure, but with remapped keys and a new namespace.

For more details, see the code and the accompanying tests in test_ports_xml_parser.py.

exception py_trees.parsers.behaviour_tree_xml.XMLParserError[source]

Bases: Exception

Exception raised due to XML parsing failure.

py_trees.parsers.behaviour_tree_xml.add_new_key_to_remapping_table(value: str, remapping_table: dict[str, str], subtree_namespace: str) None[source]

Add the value to the remapping table, if it is a key itself.

If we encounter a remapping in a SubTree or PortsMixin-derived tag, e.g. remapped_key={other_key} and the value (i.e. {other_key}) is a key itself, and this key is not yet in the remapping table, then it means that in the current subtree namespace, we have encountered this key for the first time.

Example: ` <SubTree ID="subtree1" in="{other_key}"/> `

The first time we parse a tag which has a key in the assigned value, and the key (i.e. {other_key}) is not yet in the remapping table, it needs to be added, within the scope of the subtree namespace.

Parameters:
  • value (str) – The value of the remapping, e.g. {other_key} in the statement remapped_key={other_key}.

  • remapping_table (dict[str, str]) – The remapping table.

  • subtree_namespace (str) – The current subtree namespace.

Return type:

None

py_trees.parsers.behaviour_tree_xml.build_bt_index(root: Element) dict[str, xml.etree.ElementTree.Element][source]

Return {ID: element} for every <BehaviorTree> child of root.

Parameters:

root (Element) –

Return type:

dict[str, xml.etree.ElementTree.Element]

py_trees.parsers.behaviour_tree_xml.build_port_remappings(elem: ~xml.etree.ElementTree.Element, class_: type[py_trees.ports.PortsMixin], remapping_table: dict[str, str], subtree_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>) dict[str, str][source]

Build {port_name -> absolute_key} for any PortsMixin node from XML attributes.

Mirrors the logic used for PortsMixin leaves:

  • Attributes must correspond to declared input/output ports (otherwise NotImplementedError).

  • Each attribute value may be a "{key}" reference or a direct value; both are resolved to absolute keys via the remapping table (adding entries as needed).

  • If the natural in-namespace key (/{ns}/{port}) differs from the resolved absolute key, a remapping is recorded.

Parameters:
Return type:

dict[str, str]

py_trees.parsers.behaviour_tree_xml.build_subtree_remapping(elem: ~xml.etree.ElementTree.Element, remapping_table: dict[str, str], parent_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>) dict[str, str][source]

Process the <SubTree> XML element.

The remapping table is updated to include the remappings from the <subtreeplus> or <subtree> element. The subtree is then instantiated with the new remapping table.

Parameters:
  • elem (Element) – The <SubTree> (or <SubTreePlus>) XML element.

  • remapping_table (dict[str, str]) – Parent remapping table (logical name -> absolute key).

  • parent_namespace (str) – Absolute namespace of the parent tree.

  • logger (PortsLogger) – Optional logger.

Returns:

The local remapping for this subtree’s ports.

Return type:

dict[str, str]

Raises:
  • ValueError – If a referenced key is missing or a value can’t be resolved.

  • RuntimeError – If cyclic remapping is detected during resolution.

py_trees.parsers.behaviour_tree_xml.build_tree_from_xml(elem: ~xml.etree.ElementTree.Element, remapping_table: dict[str, str], node_registry: dict, bt_index: dict, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>, subtree_namespace: str = '/', parent_names_str: str = '') Behaviour[source]

Recursively build the tree from XML element.

Parameters:
  • elem (Element) – XML element

  • remapping_table (dict[str, str]) – Remapping table.

  • node_registry (dict) – Mapping from class names (str) to callables (constructors or partials) that return PortsMixin-derived instances.

  • bt_index (dict[str, BehaviorTree]) – dictionary {ID: BehaviorTree element} for subtree lookup.

  • subtree_namespace (str) – current blackboard namespace.

  • logger (PortsLogger) – Optional logger-like object.

  • parent_names_str (str) – Dot-separated string of parent names for logging context and generating node names.

Returns:

py_trees.behaviour.Behaviour

Raises:
Return type:

Behaviour

py_trees.parsers.behaviour_tree_xml.get_absolute_reference(value: str, subtree_namespace: str) str[source]

Get the absolute path of a value (e.g. a key) by prepending the namespace.

Parameters:
  • value (str) – The value to make absolute.

  • subtree_namespace (str) – The current subtree namespace.

Returns:

The absolute reference to the value.

Return type:

str

py_trees.parsers.behaviour_tree_xml.get_class_from_registry(class_name: str, node_registry: dict) type[py_trees.ports.PortsMixin][source]

Get the class from the node_registry dictionary, ensuring it is a subclass of PortsMixin.

Parameters:
  • class_name (str) – The name of the class to look up.

  • node_registry (dict) – A dictionary mapping class names to class constructors or partial callables, e.g. {"Producer": Producer, "Consumer": partial(Consumer, name=name)}.

Returns:

The class (subclass of PortsMixin).

Raises:
  • KeyError – If the class name is not found in the node_registry.

  • TypeError – If the entry is not a class or partial callable, or if the class is not a subclass of PortsMixin.

Return type:

type[py_trees.ports.PortsMixin]

py_trees.parsers.behaviour_tree_xml.get_key_name(value: str) str[source]

Extract the key name from a {key} reference string.

Parameters:

value (str) –

Return type:

str

py_trees.parsers.behaviour_tree_xml.instantiate_ports_node(elem: ~xml.etree.ElementTree.Element, cls: type[py_trees.ports.PortsMixin], node_registry: dict, remapping_table: dict[str, str], subtree_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>, constructor_kwargs: dict | None = None, parent_names_str: str = '') PortsMixin[source]

Instantiate any PortsMixin-based node (leaf or composite).

  • looks up the class via node_registry (validated with get_class_from_registry)

  • builds port remappings from elem.attrib

  • constructs the instance (using name attribute or class_name)

  • calls setup_ports(…)

Parameters:
  • elem (Element) – The XML element to parse.

  • cls (type[py_trees.behavious.Behaviour]) – A behaviour class type.

  • node_registry (dict) – Mapping from class names (str) to callables (constructors or partials) that return PortsMixin-derived instances.

  • remapping_table (dict) – Mapping from keys (str) to absolute keys (str).

  • subtree_namespace (str) – The namespace for this subtree.

  • logger (PortsLogger) – Optional logger-like object.

  • constructor_kwargs (dict | None) – Additional keyword arguments to pass to the constructor.

  • parent_names_str (str) – Dot-separated string of parent names for logging context and generating node names.

Returns:

the fully initialised node.

Return type:

PortsMixin

py_trees.parsers.behaviour_tree_xml.is_key(value: str) re.Match | None[source]

Return a regex match if value is a {key} reference.

Parameters:

value (str) –

Return type:

re.Match | None

py_trees.parsers.behaviour_tree_xml.parse_behaviour_tree_xml(xml_file: str, main_tree_id: str | None = None, node_registry: dict | str = 'auto', logger: py_trees.ports_utils.PortsLogger | None = None, search_paths: list[str] | None = None) Behaviour[source]

Parse the XML file and build the behavior tree.

Supports simple top-level imports via:

<Import src=”other.xml”/> <Include file=”other.xml”/>

The import pre-pass inlines all <BehaviorTree> elements from the referenced XMLs into the current document. If any imported BehaviorTree ID already exists, a ValueError is raised.

Node classes are resolved from node_registry, which is either:

  • "auto" (the default) — resolve every XML tag from the global registry of auto-registered PortsMixin subclasses. Importing your node classes is enough to make them usable; no explicit mapping is needed.

  • a dict mapping tag -> class / callable — use exactly this mapping and ignore the auto-registry. Use this to inject dependencies via functools.partial (e.g. {"Wait": partial(Wait, factory=f)}), to alias tags, or to sandbox the parser to a fixed set of classes.

To combine auto-registration with a few explicit entries, build the dict on top of the auto-registry:

node_registry={**py_trees.ports.get_ports_registry(), "Wait": partial(Wait, factory=f)}
Parameters:
  • xml_file (str) – Path to the main XML file.

  • main_tree_id (str | None) – ID of the tree to execute; if None, read from ‘main_tree_to_execute’.

  • node_registry (dict | str) – "auto" to resolve from the auto-registry, or a {tag: class/partial} dict to use exclusively. Defaults to "auto".

  • logger (PortsLogger | None) – Optional logger (NoOp if None).

  • search_paths (list[str] | None) – Optional extra directories to resolve imports.

Returns:

The root py_trees.behaviour.Behaviour for the requested tree.

Raises:
  • ValueError – If node_registry is a string other than "auto", if no node classes are available, or the main BehaviorTree ID is not found.

  • TypeError – If node_registry is neither a dict nor a string.

  • FileNotFoundError / RuntimeError – From the import pre-pass if relevant.

Return type:

Behaviour

py_trees.parsers.behaviour_tree_xml.resolve_direct_value_remapping(key: str, remapping_table: dict[str, str]) str[source]

Obtain a direct value from the remapping table.

Parameters:
  • key (str) – The direct value.

  • remapping_table (dict[str, str]) – A mapping of prefixed keys to their resolved values.

Returns:

The resolved value from the remapping table.

Raises:

ValueError – If the prefixed key is not found in the remapping table or is of invalid format.

Return type:

str

py_trees.parsers.behaviour_tree_xml.resolve_key_remapping(key: str, remapping_table: dict[str, str]) str[source]

Recursively resolve a key through the remapping table until it is not a curly-brace key.

Cyclic remappings are explicitly checked and will raise a RuntimeError if detected.

Parameters:
  • key (str) – Input key. Expected to be either a curly-brace key or an absolute key.

  • remapping_table (dict[str, str]) – Maps logical keys to absolute keys.

Returns:

The resolved absolute key.

Return type:

str

Raises: