DEV Community

Marvin Hermann
Marvin Hermann

Posted on • Edited on

Python for Robotic Engineering – A Structured Foundation

Last Updated: 10.07.2025

This article is part of my Road to Emotional AI series. Follow me to watch my journey unfold.


Why Python?

Python is the structural backbone of most modern AI and robotics research. It’s readable, flexible, and perfectly suited for rapid prototyping. This post serves as my evolving knowledge base for all things Python that are relevant to robotic system engineering and scientific software architecture.


Environment Setup

Create a virtual environment

python3 -m venv venv

Activate the environment (Linux/macOS)

source venv/bin/activate

Always activate from the parent directory of the /venv folder.

Python Class / Packages Location

In the venv's parent folder


Dependency Management

Generate requirements.txt

In the venv's parent folder
pip freeze > requirements.txt

Install from requirements.txt

pip install -r requirements.txt

This ensures full reproducibility across systems (e.g., Git clones).


Clean Code Conventions (Pythonic Style Guide)

Naming

File Names

Use snake_case.py

Class Names

Use class PascalCase

Should be a noun

Method Names

Use def snake_case

Should describe the purpose of the method

Variable Names

temperature, sensor_id

Describe the contents precisely

Visibility

There is no real private, protected, public in Python. Only conventional indicators through naming

self.sensor_name      # public
self._sensor_name     # protected
self.__sensor_name    # private
Enter fullscreen mode Exit fullscreen mode

applies to variables, methods and classes


Smart Property Design (No Getter/Setter usage)

Use @property decorators

@property
def temperature(self): 
    return self._temperature

@temperature.setter
def temperature(self, value):
    self._temperature = value`

print(sensor.temperature) # Getter usage
sensor.temperature = 22.5 # Setter usage
Enter fullscreen mode Exit fullscreen mode

Dunder Methods

Special methods in Python that are automatically invoked by the interpreter during certain operations.

They start and end with` "__"

They are not just naming conventions -> they define language-level behavior

Examples:

  • __init__(self, ...): Constructor, called by instantiation
  • __str__(self): Defines the string shown when using print(obj)
  • __repr__(self): Defines how the object is represented in the shell/debugger
  • __len__(self), __getitem__, __setitem__: Enable len(obj), obj[i] = val, etc.
  • __eq__, __lt__, __gt__: Comparison operators like ==, <, >

Constructors, Inheritance and Method overriding

python
class Sensor:
def __init__(self, type):
self._sensor_type = type # "self" stores var in the object & makes it accessible across methods

Inheritance in Python allows classes to extend or specialize behavior from a parent class.
-> should only be used when it is semantically justified

Creating a Child Class

To inherit from a parent class, include its name in parentheses

python
class TemperatureSensor(Sensor):

The child class now has access to all public and protected methods and attributes of the parent
These inherited methods can be used directly or overridden by redefining them in the child. Overriding means the parent’s method is replaced but the original can still be accessed via
super()

Using super() to Access the Parent Implementation

If a method is overridden, but you still want to invoke the parent version, use super()

python
super().method_name()

This is particularly common in constructors when the child class extends the initialization logic of the parent

`python
class Sensor:
def init(self, sensor_type):
self._sensor_type = sensor_type

def read(self):
    raise NotImplementedError("Must be implemented by subclass")
Enter fullscreen mode Exit fullscreen mode

class TemperatureSensor(Sensor):
def init(self, sensor_type, calibration):
super().init(sensor_type) # Call parent constructor
self._calibration = calibration # Extend with new attribute

def read(self):
    # Custom implementation overriding the abstract parent method
    return f"{self._sensor_type} reading: {round(random.uniform(20.0, 30.0), 2)}°C"
Enter fullscreen mode Exit fullscreen mode

`


Importing and instantiating Classes

To use a class defined in another file (module), import it using Python's module system

`python
from my_module import MyClass

obj = MyClass(type)
`

This imports MyClass from the file my_module.py, and instantiates it by calling its constructor.

Execution Entry

Python uses a conditional entry point that resembles Java's main() function, but it’s not a function, and it is written at the top level of the file, not inside a class

python
if __name__ == "__main__":
my_instance.run()

This conditional ensures that the code block only runs when the file is executed directly, and not when it is imported as a module


Essential Built-In Functions

File Handling with Auto-Close

The with open(...) as ... pattern ensures that a file is automatically closed, even in case of errors –> no need to manually call close()

python
with open("file.txt", "a") as f:
f.write("New log entry\n")

Mode Purpose
"r" Read (default)
"w" Write (overwrite file)
"a" Append (add to file)
"x" Write only if file doesn't exist
"b" Binary mode

Random Numbers

python
import random
random.uniform(a, b) # Returns a float between a and b

Rounding Numbers

python
round(float, n) # Rounds a float to
ndecimal places.

Sleep / Timing

Essential when ticking loops, simulating delays, or rate-limiting processes

`python
import time

time.sleep(1.5) # Pause the program for 1.5 seconds
`

Efficient String Building

Use f-string` to build dynamic messages in a readable and performant way

=> Favor f"" formatting whenever variables are included. It's faster and clearer than traditional % formatting or .format() calls

sensor_id = "TS-01"
temp = 24.7
unit = "°C"

msg = f"[{sensor_id}] Temperature: {temp:.1f}{unit}"
print(msg)
Enter fullscreen mode Exit fullscreen mode

also usefull: " ".join([...]) for list-based strings


Flexible Function Parameters

Python offers two ways to pass an arbitrary number of arguments

*args — Positional Argument Collection

To use when expecting multiple values of the same kind

def log_multiple(*messages):
    for m in messages:
        print(m)

log_multiple("Hi", "I'm", "cool")`
Enter fullscreen mode Exit fullscreen mode

**kwargs — Named Arguments (Dictionary)

To use when expecting various named values
=> use .items() to read them

def configure_sensor(**settings):
    for key, value in settings.items():
        print(f"{key} = {value}")

configure_sensor(unit="Celsius", interval=5, active=True)`
Enter fullscreen mode Exit fullscreen mode

Lambda Functions

Use for short, one-off functions passed as arguments

`square_and_add = lambda x, y: x * x + y print(square_and_add(3, 2))`
Enter fullscreen mode Exit fullscreen mode

Great for filtering, mapping, or sorting

`result = list(filter(lambda x: x > 10, data))`
Enter fullscreen mode Exit fullscreen mode

Working with Lists

List Comprehension

A readable way to transform lists

squares = [x**2 for x in range(5)]
Enter fullscreen mode Exit fullscreen mode

map()

Applies a function to every element

map(function,  liste)
Enter fullscreen mode Exit fullscreen mode

filter()

Filters out elements where the function returns False

high_values = list(filter(lambda x: x > 10, [5, 12, 17, 3]))
Enter fullscreen mode Exit fullscreen mode

For-Loops, Range, Enumerate and Zip

Repetition

for i in range(3):
    print(i)
Enter fullscreen mode Exit fullscreen mode

Iterate Through a List

for item in my_list:
    print(item)
Enter fullscreen mode Exit fullscreen mode

Index + Value: enumerate()

for i, val in enumerate(my_list):
    print(i, val)
Enter fullscreen mode Exit fullscreen mode

Parallel Iteration: zip()

Iterates through 2 lists

sensor_ids = ["TS-01", "TS-02"]
temperatures = [22.5, 27.5]

for sensor, temp in zip(sensor_ids, temperatures):
    print(f"{sensor}: {temp}°C")
Enter fullscreen mode Exit fullscreen mode

Exception Handling

Use try/except for anything that interacts with unpredictable systems: files, hardware, APIs, users

try:
    risky_code()
except ValueError as e:
    print(f"Oops! Something went wrong: {e}")
Enter fullscreen mode Exit fullscreen mode

__repr__() — Debugging Output

When print(obj) yields a cryptic memory address e.g. 0x7f9a324e7ac0, implement

def __repr__(self):
    return f"Sensor(type={self._sensor_type})"
Enter fullscreen mode Exit fullscreen mode

This defines how the object shows up in debug output or print statements


PyTrees

Enable building of state maschines

What Are PyTrees?

  • Blackboard: Shared memory across nodes
  • Behavior Nodes:
    • Action Nodes: Perform operations
    • Condition Nodes: Evaluate transitions
    • Composite Nodes: Control flow logic (Selector, Sequence, Parallel)

Key Concepts and Conventions

  • Nodes must be manually ticked to update => fires uptade function of each node
  • Nodes are separated into individual files/modules
  • A TreeFactory centralizes construction logic
  • main.py acts as the runtime controller
  • A Client system is used to access Blackboard data

General organization

my_project/
├── nodes/
│ ├── condition_is_grounded.py
│ ├── condition_target_in_range.py
│ ├── action_move_forward.py
│ ├── action_cast_spell.py
├── tree_factory.py
├── sensor_writer.py
├── target_writer.py
├── main.py

Condition Nodes & Read/Write Clients

Condition nodes are responsible for checking variables that determine whether a behavior tree transition should be triggered

To achieve this, a client-based blackboard system is used to create, read, and write shared variables.
These variables are exclusively accessed via the tree architecture, not through direct object references

Within this system:

  • Condition nodes declare their clients with Access.READ → they consume values
  • External components (e.g., sensors or trackers) declare clients with Access.WRITE → they publish values
import py_trees  # Import

class IsGrounded(py_trees.behaviour.Behaviour):  # Inherit from py_trees base behavior
    def __init__(self):
        super().__init__(name = "Is Grounded?")  # Define the node's name for logging and visualization

        # Create a blackboard client that will access shared variables
        self.blackboard = py_trees.blackboard.Client(name = "IsGroundedClient")

        # Register a variable on the blackboard in read-only mode
        # This variable is assumed to be written by another component (e.g., a sensor class)
        self.blackboard.register_key(
            key = "is_grounded", 
            access = py_trees.common.Access.READ
        )

    def update(self):
        # Condition nodes return either SUCCESS or FAILURE depending on the variable's state
        if self.blackboard.is_grounded:
            self.logger.debug("Robot is grounded")
            return py_trees.common.Status.SUCCESS
        else:
            self.logger.debug("Robot is NOT grounded")
            return py_trees.common.Status.FAILURE
Enter fullscreen mode Exit fullscreen mode

The WRITE classes interact with the blackboard by registering and updating shared state variables. These classes serve as data providers

import py_trees

class SensorProcessor:
    def __init__(self):
        # Instantiate a blackboard client with write access
        self.blackboard = py_trees.blackboard.Client(name = "SensorWriter")

        # Register the shared variable to be writable
        self.blackboard.register_key(
            key = "is_grounded", 
            access = py_trees.common.Access.WRITE
        )

    def update_grounded_status(self, sensor_value: bool):
        # Write the updated grounded status to the blackboard
        self.blackboard.is_grounded = sensor_value

class TargetTracker:
    def __init__(self):
        # Create a separate blackboard client for target-related data
        self.bb = py_trees.blackboard.Client(name = "TargetWriter")

        # Register the variable as writable
        self.bb.register_key(
            key = "target_in_range", 
            access = py_trees.common.Access.WRITE
        )

    def update_target_range(self, in_range: bool):
        # Update target visibility status
        self.bb.target_in_range = in_range
Enter fullscreen mode Exit fullscreen mode

Action Nodes

Action Nodes are responsible for executing concrete behaviors within the tree. They typically follow a lifecycle of initialization, execution, and termination, reporting their current status via the standard py_trees return codes

  • RUNNING while the action is in progress
  • SUCCESS when the action completes successfully
  • FAILURE if the action cannot be carried out

Internally, these nodes usually act as interfaces that trigger methods of other classes to perform the actual logic

import py_trees
import time

class MoveForward(py_trees.behaviour.Behaviour):
    def __init__(self):
        # Define the name of this node in the tree
        super().__init__(name="Move Forward")
        self.started = False

    def initialise(self):
        # Called every time the node is entered (ticked for the first time)
        self.started = False

    def update(self):
        # If this is the first tick, start the action
        if not self.started:
            print("Starting forward motion...")
            self.started = True
            self.start_time = time.time()
            return py_trees.common.Status.RUNNING

        # If the action has been running for more than 2 seconds, consider it done
        if time.time() - self.start_time > 2.0:
            print("Forward motion complete.")
            return py_trees.common.Status.SUCCESS

        # Otherwise, the action is still running
        return py_trees.common.Status.RUNNING
Enter fullscreen mode Exit fullscreen mode

Factory Setup

The Tree Factory pattern abstracts the construction of complex behavior trees into a centralized module
This allows trees to be declared once and instantiated dynamically at runtime -> improving modularity, testability, and clarity of system design

Each factory method creates and returns a predefined subtree composed of condition and action nodes

from py_trees.composites import Selector, Sequence, Parallel
from py_trees.common import ParallelPolicy

# Import custom nodes
from nodes.condition_is_grounded import IsGrounded
from nodes.action_move_forward import MoveForward
from nodes.condition_is_grounded import IsTargetInRange
from nodes.action_move_forward import ScanTarget

class TreeFactory:

    @staticmethod
    def create_locomotion_tree():
        # Selector runs children in order and returns on first SUCCESS
        root = Selector(name = "Locomotion")
        root.add_children([
            IsGrounded(),
            MoveForward()
        ])
        return root

    @staticmethod
    def create_ability_tree():
        # Sequence requires all children to return SUCCESS (in order)
        root = Sequence(name = "Abilities")
        root.add_children([
            IsTargetInRange(),
            ScanTarget()
        ])
        return root

    @staticmethod
    def create_root_parallel_tree():
        # Parallel node ticks all children simultaneously
        root = Parallel(
            name = "Root Layer",
            policy = ParallelPolicy.SuccessOnAll()  # All subtrees must return SUCCESS
        )
        root.add_children([
            TreeFactory.create_locomotion_tree(),
            TreeFactory.create_ability_tree()
        ])
        return root

Enter fullscreen mode Exit fullscreen mode

Tree Execution example

import time
import py_trees

from tree_factory import TreeFactory        # Factory providing predefined subtrees
from sensor_writer import SensorProcessor   # Blackboard WRITE client for sensor state
from target_writer import TargetTracker     # Blackboard WRITE client for target state

def main():
    # Create the root tree which runs multiple subtrees in parallel
    root = TreeFactory.create_root_parallel_tree()
    tree = py_trees.trees.BehaviourTree(root)

    # Initialize writer classes responsible for updating the blackboard
    sensor = SensorProcessor()
    tracker = TargetTracker()

    # Set initial blackboard states
    sensor.update_grounded_status(True)
    tracker.update_target_range(False)

    print("=== Starting Tree Execution ===")
    for i in range(10):
        # Dynamically modify blackboard states during runtime
        if i == 5:
            sensor.update_grounded_status(False)
            tracker.update_target_range(True)

        print(f"\n--- Tick {i} ---")
        tree.tick()
        time.sleep(1.0)

if __name__ == "__main__":
    main()

Enter fullscreen mode Exit fullscreen mode

Execution Logic of Behavior Trees

Each tree node decides what to do next, based on structural rules and runtime conditions

Root as the Primary Branching Point

In most setups, the root node is a Selector, which acts as the central decision-maker
It attempts each of its children in order, and selects the first one whose internal condition returns SUCCESS

RootSelector
├── Combat Mode
├── Charge Battery
├── Explore
└── Idle

Only the first successful path is ticked. Remaining branches are skipped unless the current one fails in future ticks

This makes selectors ideal for building priority-based fallback systems


Example: Combat Mode as a Sequence

Sequence("Combat Mode")
├── Condition: Enemy Detected
├── Selector
│ ├── Use Ranged
│ └── Use Melee

  • If Enemy Detected returns FAILURE, the entire sequence halts
  • If it returns SUCCESS, the inner selector ticks and evaluates its options: It may choose Use Ranged or Use Melee, depending on availability or cooldowns

What if Multiple States Are True Simultaneously?

In cases where multiple branches should execute in parallel a Selector is no longer sufficient

Instead, use a Parallel Node:

root = py_trees.composites.Parallel(
    name="Root",
    policy=py_trees.common.ParallelPolicy.SuccessOnAll()
)
Enter fullscreen mode Exit fullscreen mode
  • All children of the parallel node are ticked simultaneously
  • Each subtree evaluates independently, based on its own local conditions

This mirrors a layered control model, like Unity’s behavior layers


Final Words

This post evolves as I evolve. I will continuously refine and expand it as I deepen my understanding. Feedback and suggestions are always welcome!

Top comments (0)