Python vs C++ for Robotics: Which Should You Use?

Use C++ for anything inside a hard timing loop: motor control, sensor drivers, point cloud processing, and SLAM back ends. Use Python for everything above that loop: perception prototypes, mission logic, tooling, and ML inference. Most production robots use both, joined by ROS2 topics, services, or a thin binding layer.

Quick Takeaways

  • C++ gives deterministic latency, direct memory control, and sub-millisecond loops on a PREEMPT_RT kernel.
  • Python cuts development time sharply and owns the ML ecosystem (PyTorch, OpenCV bindings, NumPy).
  • Run control loops above ~200Hz, or anything with hard deadlines, in C++. Run planners, behavior trees, and UI glue in Python.
  • The best architecture is hybrid: C++ nodes for the real-time core, Python nodes for high-level logic.
Criterion Python (rclpy) C++ (rclcpp)
Loop jitter (non-RT Linux) Typically 1-10 ms Typically 50-500 µs
Max practical control rate ~100-200Hz 1kHz+ (with RT kernel)
Memory control Garbage collected Manual / RAII
Prototyping speed Fast Slower (compile cycle)
ML / vision ecosystem Excellent Good (LibTorch, OpenCV)
Microcontroller support MicroPython (limited) Native (micro-ROS, Arduino, STM32)
Learning curve Low Steep
Debugging Easy (REPL, pdb) Harder (gdb, sanitizers)

Latency figures are typical ranges on commodity hardware. Your numbers depend on CPU, kernel configuration, and message size, so measure on your own target.

Why Execution Model Matters in Robotics

A robot is a closed-loop system. The controller reads state, computes an error, and commands an actuator, all within a fixed period T. If the computation misses its deadline, the effective control gain shifts and the loop can go unstable.

For a discrete PID controller:

e[k]  = r[k] - y[k]
u[k]  = Kp * e[k] + Ki * T * Σ e[i] + Kd * (e[k] - e[k-1]) / T

The T in the integral and derivative terms assumes a constant sample period. If the real period varies, the computed derivative picks up noise of the form:

Δ(derivative error) ≈ (e[k] - e[k-1]) * (1/T_actual - 1/T_nominal)

A 500Hz loop has a 2 ms budget. If Python’s garbage collector pauses for 5 ms, one sample is lost. On a fast BLDC motor current loop, that is a real fault, not a cosmetic glitch.

Where Python Loses Time

  • Global Interpreter Lock (GIL): Threads do not run Python bytecode in parallel, so executor threads in rclpy contend with each other.
  • Garbage collection: Pauses are non-deterministic.
  • Serialization overhead: rclpy converts messages between Python objects and C structs on every callback.
  • Dynamic dispatch: Each operation costs more than a compiled call.

Where C++ Wins

  • Zero-copy transport: rclcpp supports intra-process communication and loaned messages, which avoids serialization entirely.
  • Deterministic allocation: Pre-allocate buffers before the loop starts.
  • Real-time scheduling: Pair SCHED_FIFO threads with mlockall() to prevent page faults.
  • SIMD and Eigen: Matrix math for kinematics runs several times faster than interpreted NumPy loops on small matrices.

Same Controller, Two Languages

Both examples implement a velocity PID node. The Float64 messages carry measured velocity in and motor command out.

Python (rclpy)

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64


class PidNode(Node):
    def __init__(self):
        super().__init__('pid_py')
        # Gains exposed as ROS2 parameters for runtime tuning
        self.declare_parameter('kp', 1.2)
        self.declare_parameter('ki', 0.4)
        self.declare_parameter('kd', 0.02)
        self.declare_parameter('setpoint', 1.0)  # target velocity, m/s

        self.measured = 0.0
        self.integral = 0.0
        self.prev_error = 0.0
        self.dt = 0.01  # 100Hz: realistic ceiling for rclpy

        self.create_subscription(Float64, 'measured_velocity', self.on_meas, 10)
        self.pub = self.create_publisher(Float64, 'motor_cmd', 10)
        self.create_timer(self.dt, self.update)

    def on_meas(self, msg: Float64):
        self.measured = msg.data  # cache latest sensor value

    def update(self):
        kp = self.get_parameter('kp').value
        ki = self.get_parameter('ki').value
        kd = self.get_parameter('kd').value
        sp = self.get_parameter('setpoint').value

        error = sp - self.measured
        self.integral += error * self.dt
        # Anti-windup: clamp integral to +/-10 to prevent saturation drift
        self.integral = max(-10.0, min(10.0, self.integral))
        derivative = (error - self.prev_error) / self.dt
        self.prev_error = error

        out = Float64()
        out.data = kp * error + ki * self.integral + kd * derivative
        self.pub.publish(out)


def main():
    rclpy.init()
    rclpy.spin(PidNode())
    rclpy.shutdown()


if __name__ == '__main__':
    main()

C++ (rclcpp)

#include <chrono>
#include <algorithm>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float64.hpp"

using namespace std::chrono_literals;

class PidNode : public rclcpp::Node {
public:
  PidNode() : Node("pid_cpp") {
    declare_parameter("kp", 1.2);
    declare_parameter("ki", 0.4);
    declare_parameter("kd", 0.02);
    declare_parameter("setpoint", 1.0);

    sub_ = create_subscription<std_msgs::msg::Float64>(
      "measured_velocity", rclcpp::SensorDataQoS(),   // best-effort QoS for low latency
      [this](std_msgs::msg::Float64::SharedPtr m) { measured_ = m->data; });
    pub_ = create_publisher<std_msgs::msg::Float64>("motor_cmd", rclcpp::SensorDataQoS());
    // 1ms period = 1kHz loop; wall timer, so run on an RT-scheduled thread
    timer_ = create_wall_timer(1ms, [this]() { update(); });
  }

private:
  void update() {
    const double dt = 0.001;
    const double error = get_parameter("setpoint").as_double() - measured_;
    integral_ = std::clamp(integral_ + error * dt, -10.0, 10.0);  // anti-windup
    const double derivative = (error - prev_error_) / dt;
    prev_error_ = error;

    std_msgs::msg::Float64 out;
    out.data = get_parameter("kp").as_double() * error
             + get_parameter("ki").as_double() * integral_
             + get_parameter("kd").as_double() * derivative;
    pub_->publish(out);
  }

  double measured_{0.0}, integral_{0.0}, prev_error_{0.0};
  rclcpp::Subscription<std_msgs::msg::Float64>::SharedPtr sub_;
  rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr pub_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char ** argv) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<PidNode>());
  rclcpp::shutdown();
  return 0;
}

In the C++ version, get_parameter() inside the loop is a lock-protected lookup. For true hard real-time work, cache gains in member variables and update them from a parameter callback.

Performance by Robotics Subsystem

Subsystem Typical Rate Recommended Language Reason
Motor current / torque loop 5-20kHz C/C++ (MCU firmware) Hard deadline, no OS
Joint position/velocity loop 500Hz-1kHz C++ Jitter-sensitive
IMU + wheel odometry EKF 100-400Hz C++ Matrix math, fixed dt
LiDAR point cloud filtering 10-30Hz, 100k+ pts C++ (PCL) Memory bandwidth bound
Visual detection (YOLO, etc.) 10-60Hz Python or C++ GPU does the work; either works
Path planning (global) 1-5Hz Python or C++ Not timing critical
Behavior trees / state machines Event-driven Python Logic changes constantly
Fleet dashboards, logging, tools N/A Python Library ecosystem

Kinematics: A Language Comparison in Practice

Take a 2-link planar arm with link lengths L1 and L2. The inverse kinematics solution for target (x, y) is:

D  = (x² + y² - L1² - L2²) / (2 * L1 * L2)
θ2 = atan2(±√(1 - D²), D)
θ1 = atan2(y, x) - atan2(L2 * sin(θ2), L1 + L2 * cos(θ2))

Python handles this in four lines of NumPy and is ideal for exploring workspace limits. For a 7-DOF arm solved at 1kHz using the Jacobian pseudo-inverse, you need Eigen in C++:

Δq = J⁺ · Δx,   where J⁺ = Jᵀ (J Jᵀ + λ²I)⁻¹   (damped least squares)

The damping term λ avoids singularity blowup near workspace boundaries. Libraries such as KDL, Pinocchio, and MoveIt’s kinematics plugins are all C++ under the hood, and their Python wrappers call the same compiled code.

Building a Hybrid Architecture

Most mature robot stacks look like this:

┌────────────────────────────────────────────────┐
│  Python: mission logic, behavior tree, ML      │  1-30Hz
├────────────────────────────────────────────────┤
│  ROS2 DDS (topics / services / actions)        │
├────────────────────────────────────────────────┤
│  C++: SLAM, EKF, planners, ros2_control        │  50-1000Hz
├────────────────────────────────────────────────┤
│  C / C++ firmware (micro-ROS, STM32, ESP32)    │  1-20kHz
└────────────────────────────────────────────────┘

Binding Python to C++ with pybind11

If you need a compiled algorithm callable from a Python script, wrap it:

// kinematics_bind.cpp
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <Eigen/Dense>

// Damped least squares step: dq = J^T (J J^T + lambda^2 I)^-1 * dx
Eigen::VectorXd dls_step(const Eigen::MatrixXd& J,
                         const Eigen::VectorXd& dx,
                         double lambda) {
  Eigen::MatrixXd A = J * J.transpose()
                    + lambda * lambda * Eigen::MatrixXd::Identity(J.rows(), J.rows());
  return J.transpose() * A.ldlt().solve(dx);
}

PYBIND11_MODULE(kinematics_bind, m) {
  m.def("dls_step", &dls_step, "Damped least squares IK step");
}

Python then calls kinematics_bind.dls_step(J, dx, 0.05) with NumPy arrays and pays only a small conversion cost.

Real-World Workflow: Making a Python Prototype Real-Time

A common path for a mobile robot team:

  1. Prototype in Python. Build the differential-drive controller as an rclpy node. Tune Kp, Ki, Kd live with ros2 param set.
  2. Log and measure. Record a rosbag2 and compute loop-period variance with ros2 topic delay and ros2 topic hz.
  3. Identify the bottleneck. If period standard deviation exceeds ~10% of nominal, port that node. Profile with py-spy first, since a hot path may be a single loop you can vectorize.
  4. Port the hot node to C++. Keep the same topic names and message types. Python consumers need no changes.
  5. Harden the C++ node. Install a PREEMPT_RT kernel. Set the control thread to SCHED_FIFO priority 80. Call mlockall(MCL_CURRENT | MCL_FUTURE). Pin it to an isolated core with isolcpus.
  6. Validate. Run cyclictest under load and confirm worst-case latency stays well under your loop period.
Stage Tool Target Metric
Prototype rclpy, ros2 param Step response behaves
Profile py-spy, ros2 topic hz Jitter < 10% of period
Port rclcpp, ament_cmake Identical topic interface
Harden PREEMPT_RT, SCHED_FIFO cyclictest max < 100 µs

Decision Framework

Your Situation Choose
Hobby robot, Raspberry Pi, <100Hz loops Python
Learning ROS2 fundamentals Python first, C++ second
Vision or ML-heavy pipeline Python (+ C++ for deployment)
Industrial arm, 1kHz joint control C++
Custom motor drivers, firmware C/C++
Large point clouds, SLAM back end C++
Research prototype, algorithm exploration Python
Commercial product with safety requirements C++ core, Python tooling

Two additional factors matter. Hiring is one: robotics software roles are weighted heavily toward C++ for production, while research labs lean on Python. Safety certification is the other: standards-driven projects (for example, those following MISRA C++ guidelines) are far easier to justify in C++ than in a garbage-collected runtime.

Common Mistakes

  • Running a 1kHz loop in Python. Missed deadlines show up as jittery motors, not error messages.
  • Using C++ for everything. Mission logic changes often, and compile cycles slow experiments.
  • Ignoring QoS. Both languages need matched QoS profiles. Sensor streams should use SensorDataQoS (best-effort). Commands usually need reliable delivery.
  • Skipping the RT kernel. C++ alone does not guarantee determinism. Without PREEMPT_RT and scheduler tuning, the OS can still preempt your loop.
  • Measuring on a desktop. Test on the actual embedded target, because cache sizes and clock governors change results.

FAQ

Is Python fast enough for robotics?

Yes, for high-level tasks. Python handles planning, perception orchestration, and logic at 1-100Hz well, and GPU-accelerated ML runs at native speed because the heavy math happens in compiled libraries. It falls short for hard real-time loops above roughly 200Hz.

Do I need to learn C++ to work with ROS2?

Not to start. rclpy covers nodes, topics, services, actions, and launch files. You will need C++ to write ros2_control hardware interfaces, contribute to core packages like Nav2 or MoveIt internals, or meet strict latency requirements.

Can I use both Python and C++ in the same ROS2 project?

Yes. ROS2 nodes are language-agnostic and communicate over DDS, so a Python node can subscribe to a C++ node‘s topics with no changes. For direct function calls inside one process, use pybind11 or nanobind.

Which language do robotics companies use in production?

Most use C++ for the real-time core and Python for tooling, testing, data pipelines, and ML. Some newer projects add Rust for memory-safe components, but C++ remains dominant because of its library ecosystem (Eigen, PCL, MoveIt, Nav2, ros2_control).

Hot this week

Humanoids in Factories and Warehouses: What They Actually Do Today

Which humanoid robots do real work in factories and warehouses today? See deployment data, ROI math and ROS 2 code. Read the 2026 guide.

Debugging Robot Code: Logs, Serial Output, rosbag, and Foxglove

Debug robot code faster with ROS 2 logging, serial capture, rosbag2 recording, and Foxglove. Copy the configs and scripts into your stack today.

Build a Mapping Robot With SLAM From Scratch

Build a SLAM mapping robot from scratch: wiring, odometry, LiDAR, ROS 2 slam_toolbox config and tuning. Follow the full guide and map your first room.

Depth Cameras: Stereo, Time-of-Flight, and Structured Light Compared

Compare stereo, time-of-flight, and structured light depth cameras: accuracy, range, sunlight, ROS2 setup. Pick the right sensor for your robot.

Build a ROS 2 Rover: Raspberry Pi 5, Motors, and LiDAR

Build a ROS 2 rover with Raspberry Pi 5, RPLIDAR C1, and a Pico motor controller. Copy the code, wire it, and map your room. Start building now.

Topics

Humanoids in Factories and Warehouses: What They Actually Do Today

Which humanoid robots do real work in factories and warehouses today? See deployment data, ROI math and ROS 2 code. Read the 2026 guide.

Debugging Robot Code: Logs, Serial Output, rosbag, and Foxglove

Debug robot code faster with ROS 2 logging, serial capture, rosbag2 recording, and Foxglove. Copy the configs and scripts into your stack today.

Build a Mapping Robot With SLAM From Scratch

Build a SLAM mapping robot from scratch: wiring, odometry, LiDAR, ROS 2 slam_toolbox config and tuning. Follow the full guide and map your first room.

Depth Cameras: Stereo, Time-of-Flight, and Structured Light Compared

Compare stereo, time-of-flight, and structured light depth cameras: accuracy, range, sunlight, ROS2 setup. Pick the right sensor for your robot.

Build a ROS 2 Rover: Raspberry Pi 5, Motors, and LiDAR

Build a ROS 2 rover with Raspberry Pi 5, RPLIDAR C1, and a Pico motor controller. Copy the code, wire it, and map your room. Start building now.

Imitation Learning for Robots: Teach by Demonstration

Learn imitation learning for robots: behavior cloning, DAgger, ACT, and diffusion policies with ROS2 code. Build your first policy today.

LiDAR for Robots: 2D, 3D, and Solid-State Compared

Compare 2D, 3D, and solid-state LiDAR for robots. See specs, ROS2 configs, code, and a selection workflow. Pick the right sensor now.

ROS 2 Installation and First Steps: Nodes, Topics, and Messages

Install ROS 2 Lyrical on Ubuntu 26.04, then build your first node, topic, and custom message with working rclpy code. Follow the tutorial now.

Related Articles

Popular Categories