No module named ‘rclpy’ or ‘rclpy._rclpy_pybind11’ in ROS 2
What happened when I ran it
I started nine shells inside the official ROS 2 Jazzy container and ran python3 -c 'import rclpy' in each one. The only things I changed were the shell, the setup file and how Python was started. Six of those shells began with an empty environment, like a terminal you’ve just opened. Three of the six imported rclpy. The other three failed, and none of the failures had anything to do with the installation:
- bash, nothing sourced:
ModuleNotFoundError: No module named 'rclpy', exit code 1. sh(dash on Ubuntu) readingsetup.bash:Bad substitution, thencannot open /setup.sh, and the shell quit with exit code 2 before Python ever ran. In an interactive dash on a pseudo-terminal, the same three error lines appeared, the prompt came back, and the nextpython3printedNo module named 'rclpy'.- bash with
setup.bashsourced correctly, but Python started aspython3 -I(a flag that makes Python ignorePYTHONPATH):No module named 'rclpy'again. This was the control. It shows that the ROS entry inPYTHONPATHis the only thing that lets Python find rclpy.
Every case matched the prediction written in the plan before the runs, and all five runs gave the same results.
The test did not reproduce the other common error, No module named 'rclpy._rclpy_pybind11', because the container ships only one Python. That error has a different cause, and sourcing again won’t fix it. I cover it below with a file-name check that the container could run.
The short answer: read the last line of the traceback
“rclpy not found” shows up in two forms, and each one needs a different fix.
| Last line of the error | What it means | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'rclpy' |
This terminal’s Python doesn’t know about the ROS site-packages folder. Either you didn’t source a setup file in this terminal, or you sourced the wrong one for your shell and it failed partway through. |
In this terminal, source the file that matches your shell: setup.bash for bash, setup.zsh for zsh, setup.sh for plain sh. |
ModuleNotFoundError: No module named 'rclpy._rclpy_pybind11' plus The C extension '…' isn't present on the system |
Python found rclpy, so the source step worked. But the python3 you’re running is a different Python version from the one rclpy was built for, and the most common culprit is a conda Python. |
Use the system Python: run conda deactivate or call /usr/bin/python3 directly, and keep conda off your PATH when you use apt-installed ROS. |
For Humble on Ubuntu 22.04, the first fix looks like this:
source /opt/ros/humble/setup.bash # bash
# source /opt/ros/humble/setup.zsh # zsh
# . /opt/ros/humble/setup.sh # sh / dash
python3 -c 'import rclpy'; echo $? # 0 means the import worked
The well-known Robotics Stack Exchange question Installed ROS2 Humble but rclpy not found is actually the second kind, even though “just source it” is the usual reply. The asker had installed Humble on Ubuntu 22.04 under WSL2. Gazebo and RViz worked, but rclpy failed with No module named 'rclpy._rclpy_pybind11'. The message asked for a file ending in cpython-311-x86_64-linux-gnu.so, which is a Python 3.11 file name, while the asker reported a system Python of 3.11.5. Humble’s Ubuntu packages are built for Python 3.10. Reinstalling ROS didn’t help, which makes sense because the installation was never the problem. The other answer on that question reports that removing Anaconda fixed it.
Measured results
How the test worked: a harness ran one Python script inside the container. The harness had already sourced setup.bash, so a child shell would normally inherit a working ROS environment. To imitate a newly opened terminal, each case started a child shell with only PATH and HOME set. Each shell ran three steps: the source command (if any), an echo of PYTHONPATH, and the Python command. One case (C2) ran an interactive sh -i on a pseudo-terminal, typing one line at a time like a person would. A case counted as a success only if the exit code was 0 and Python printed its marker. The predictions were written into the plan before any run.
Environment. Image ros:jazzy-ros-base (ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca), ROS 2 Jazzy, Python 3.12.3, rclpy 7.1.12-1noble.20260902.053513, RMW rmw_fastrtps_cpp (default), no network, 1 CPU and 1 GiB. sh resolves to /usr/bin/dash, bash is /usr/bin/bash, and zsh is not installed. The clean shells got PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin and HOME=/tmp. The script (sha256 a8b509d6fb1fdf5dbf12b4c210a100a46b733a201ef64c2e76812d90299652c2) ran 5 times. All 5 runs exited with code 0, took 2.5 s to 3.22 s, and produced the same exit codes, error classes and file checks.
| Case | Shell | Source step | Python | Predicted (plan.md) | Measured, all 5 runs |
|---|---|---|---|---|---|
| A | bash, clean env | none | python3 |
fail, No module named 'rclpy' |
exit 1, No module named 'rclpy', ROS path not in PYTHONPATH |
| B | bash, clean env | source /opt/ros/jazzy/setup.bash |
python3 |
success | exit 0 |
| C | sh (dash), non-interactive, clean env | . /opt/ros/jazzy/setup.bash |
python3 |
Bad substitution, shell exits, Python never runs, non-zero exit |
exit 2, Bad substitution, shell did not reach the line after the source step |
| C2 | sh (dash), interactive on a pty, clean env | . /opt/ros/jazzy/setup.bash |
python3 |
same errors, shell survives, No module named 'rclpy' |
exit 1, shell survived, PYTHONPATH empty, No module named 'rclpy' |
| D | sh (dash), clean env | . /opt/ros/jazzy/setup.sh |
python3 |
success | exit 0 |
| E | bash, clean env | source /opt/ros/jazzy/setup.sh |
python3 |
success | exit 0 |
| F | bash, clean env | source /opt/ros/jazzy/setup.bash |
python3 -I |
fail, No module named 'rclpy' |
exit 1, No module named 'rclpy' although the ROS path was in PYTHONPATH |
| I | bash, environment inherited | none | python3 |
success | exit 0 |
| J | bash, clean env | source /opt/ros/jazzy/setup.bash |
rclpy.init(), create_node, shutdown() |
success | exit 0, node created |
Of the 6 clean-environment cases (A to F), 3 imported rclpy (B, D, E). Of all 9 cases, 5 did. 9 of 9 cases matched the prediction, and the list of mismatched cases was empty in every run.
File and shell checks (no import attempted):
- G: the interpreter’s
EXT_SUFFIXis.cpython-312-x86_64-linux-gnu.so. The only C extension in/opt/ros/jazzy/lib/python3.12/site-packages/rclpyis_rclpy_pybind11.cpython-312-x86_64-linux-gnu.so. Nocpython-310orcpython-311file exists there. Predicted: matching file present, no other tags. - H: the path that rpyutils puts in its error message,
/opt/ros/jazzy/lib/python3.12/site-packages/_rclpy_pybind11.cpython-312-x86_64-linux-gnu.so, does not exist on this working install. Predicted: absent. - K: with
SHELL=/bin/bashset, a runningshreported$SHELLas/bin/bash, butreadlink /proc/$$/exereturned/usr/bin/dash. Predicted: different. - L:
demo_nodes_pyis not installed in the ros-base image, and/opt/ros/jazzy/bin/python3does not exist. Predicted: both absent.
Raw output from run 1 (one JSON line per case):
{"kind": "case", "case": "C_sh_setup.bash", "shell": "sh", "interactive": false, "clean_env": true, "source": ". /opt/ros/jazzy/setup.bash", "python": "python3", "exit_code": 2, "shell_survived_source": false, "ros_site_in_pythonpath": false, "import_ok": false, "api_ok": false, "error_class": "bad_substitution", "stderr_lines": 3, "stderr_first": "sh: 1: /opt/ros/jazzy/setup.bash: Bad substitution", "stderr_last": "sh: 11: .: cannot open /setup.sh: No such file", "stderr_all": ["sh: 1: /opt/ros/jazzy/setup.bash: Bad substitution", "sh: 6: /opt/ros/jazzy/setup.bash: builtin: not found", "sh: 11: .: cannot open /setup.sh: No such file"], "elapsed_ms": 1.9}
{"kind": "case", "case": "C2_sh_interactive_setup.bash", "shell": "sh", "interactive": true, "clean_env": true, "source": ". /opt/ros/jazzy/setup.bash", "python": "python3", "exit_code": 1, "shell_survived_source": true, "ros_site_in_pythonpath": false, "import_ok": false, "api_ok": false, "error_class": "no_module_rclpy", "stderr_lines": 6, "stderr_first": "sh: 1: /opt/ros/jazzy/setup.bash: Bad substitution", "stderr_last": "ModuleNotFoundError: No module named 'rclpy'", "stderr_all": ["sh: 1: /opt/ros/jazzy/setup.bash: Bad substitution", "sh: 6: /opt/ros/jazzy/setup.bash: builtin: not found", "sh: 11: .: cannot open /setup.sh: No such file", "Traceback (most recent call last):", " File \"<string>\", line 1, in <module>", "ModuleNotFoundError: No module named 'rclpy'"], "elapsed_ms": 16.6}
{"kind": "case", "case": "F_bash_setup.bash_python_-I", "shell": "bash", "interactive": false, "clean_env": true, "source": "source /opt/ros/jazzy/setup.bash", "python": "python3 -I", "exit_code": 1, "shell_survived_source": true, "ros_site_in_pythonpath": true, "import_ok": false, "api_ok": false, "error_class": "no_module_rclpy", "stderr_lines": 3, "stderr_first": "Traceback (most recent call last):", "stderr_last": "ModuleNotFoundError: No module named 'rclpy'", "stderr_all": [], "elapsed_ms": 201.0}
{"kind": "file_check", "case": "G_ext_suffix_match", "ext_suffix": ".cpython-312-x86_64-linux-gnu.so", "rclpy_dir": "/opt/ros/jazzy/lib/python3.12/site-packages/rclpy", "pybind_so_files": ["_rclpy_pybind11.cpython-312-x86_64-linux-gnu.so"], "abi_tags_present": ["cpython-312"], "matching_file_exists": true, "cpython-310_exists": false, "cpython-311_exists": false}
{"kind": "file_check", "case": "H_rpyutils_message_path", "message_path": "/opt/ros/jazzy/lib/python3.12/site-packages/_rclpy_pybind11.cpython-312-x86_64-linux-gnu.so", "exists": false}
{"kind": "shell_check", "case": "K_SHELL_vs_running", "SHELL": "/bin/bash", "running_exe": "/usr/bin/dash", "same": false}
{"kind": "summary", "fresh_shell_cases": 6, "fresh_shell_import_ok": 3, "all_cases": 9, "all_import_ok": 5, "prediction_matches": 9, "mismatched_cases": [], "total_seconds": 1.76}
What the results say
Sourcing is per terminal. A and B differ only by one source line. I is the reverse check: with no source at all, a shell that inherited the environment of an already-sourced process imported rclpy fine. So “it works in my other terminal” proves nothing about this one. A terminal opened from a sourced terminal and a terminal opened fresh from your desktop can behave differently.
PYTHONPATH is the whole mechanism. In F, the echo line showed the ROS path in PYTHONPATH, yet python3 -I couldn’t find rclpy. -I ignores every PYTHON* variable (Python command line docs), so nothing else in the environment was pointing Python at ROS. Any tool that starts Python in isolated mode or with a scrubbed environment will hit the same wall.
The wrong setup file fails loudly in a script and quietly at a prompt. C and C2 read the same file with the same shell and printed the same three errors. Here is my reading of those lines against the setup.bash template. Bad substitution comes from ${BASH_SOURCE[0]} and builtin: not found comes from bash’s builtin cd. Both happen inside the command substitution that works out the install folder, so the folder comes back empty. The third line, cannot open /setup.sh, is the script trying to load "$AMENT_CURRENT_PREFIX/setup.sh" with that empty prefix. The fact that there are three lines tells you the shell kept going after the first two. According to the POSIX shell spec, section 2.8.1, a non-interactive shell exits when a special built-in such as . fails, and an interactive one doesn’t. That matches what happened. The plan had blamed the exit on Bad substitution. The outcome it predicted was right (the shell exited and Python never ran), but the log puts the exit at the third error.
The interactive case is the one people run into at a real prompt. You see a couple of error lines, you get your prompt back, and the terminal looks ready, but it has no ROS environment. If you missed the errors, the first thing you notice is No module named 'rclpy'.
setup.sh works everywhere, by design. D and E passed in both dash and bash, because setup.sh doesn’t try to locate itself. It uses the install path baked in at build time (setup.sh.in L5–L8).
echo $SHELL doesn’t tell you which shell you’re in. K shows a dash process whose $SHELL said bash. SHELL holds your preferred login shell (POSIX XBD 8, bash variables), not the program that’s running now. On Linux, use readlink /proc/$$/exe.
Why each shell needs its own setup file
source (or . in POSIX sh) runs a script inside the current shell instead of starting a new process, so the variables it sets stay in that shell (bash builtins). That’s why the ROS 2 docs tell you to run it “on every new shell you open.” Among other things, it puts /opt/ros/humble/lib/python3.10/site-packages at the front of PYTHONPATH (pythonpath.sh.in). On Jazzy that path is /opt/ros/jazzy/lib/python3.12/site-packages.
The three setup files do the same work. Only their first lines differ:
setup.bashfinds its own folder with${BASH_SOURCE[0]}, a bash-only array, and then loadssetup.shfrom that folder (setup.bash L3–L11).setup.zshdoes the same with the zsh-only expansion${(%):-%N}(setup.zsh L3–L6, zsh prompt expansion).setup.shuses a fixed install path and runs in any POSIX shell.
The Humble install guide says it directly: if you don’t use bash, replace .bash with your shell’s extension (setup.bash, setup.sh, setup.zsh).
zsh users often paste the bash line from a tutorial. I couldn’t test that, because the image has no zsh. Going by the template, though, BASH_SOURCE is empty in zsh, so dirname "" returns . and the script looks for ./setup.sh in whatever directory you’re in. Unless you happen to be in /opt/ros/humble, that should fail and leave PYTHONPATH unchanged. Treat this as a prediction, not a measurement. The fix either way is source /opt/ros/humble/setup.zsh.
The same logic covers scripts. If a teammate runs sh start_robot.sh and its first line is . /opt/ros/humble/setup.bash, that’s case C: dash stops before reaching Python (with exit code 2 in the Jazzy test). Either change the line to . /opt/ros/humble/setup.sh (case D), or run the script with bash and keep source /opt/ros/humble/setup.bash (case B).
Why sourcing again won’t fix _rclpy_pybind11
import rclpy loads a compiled C extension right away: __init__.py imports rclpy.signals, which pulls in implementation_singleton, which loads _rclpy_pybind11 (__init__.py L47–L51, signals.py L17, implementation_singleton.py L29–L32). That .so file is tied to the Python version it was built with, and the version is part of its name, for example _rclpy_pybind11.cpython-310-x86_64-linux-gnu.so (PEP 3149). An interpreter only loads extensions whose names end in one of its own suffixes (importlib.machinery.EXTENSION_SUFFIXES). To a Python 3.11, a cpython-310 file in the same folder might as well not exist.
Humble’s deb packages target Ubuntu 22.04 (REP 2000), whose python3 is 3.10 (Ubuntu jammy python3), so Humble’s rclpy folder should contain a cpython-310 file. I took that from the package metadata and didn’t run Humble. PYTHONPATH is read by every interpreter, so a conda Python 3.11 in a sourced terminal finds the pure-Python part of rclpy, and then stops at the C extension. That’s the second error. The official troubleshooting page gives the same diagnosis: you are using a different Python interpreter.
One trap in the error message: the path it prints isn’t where the file lives. rpyutils builds that path from its own location, one level up, which is site-packages/_rclpy_pybind11… (import_c_library.py L41–L57). The real file is in site-packages/rclpy/. Check H above: on a working install, the message path doesn’t exist. If you ls the path from the message, you’ll conclude the install is broken when it isn’t. Look inside the rclpy/ folder instead and compare the cpython-XY tag with your interpreter’s EXT_SUFFIX.
The fix is the one in the official docs: use the same interpreter, and when you use apt binaries, keep conda out of your PATH. That may mean commenting out the conda block in ~/.bashrc (troubleshooting L99–L104). For a quick check, run conda deactivate in that terminal, or call /usr/bin/python3 directly. A (base) prompt with the same error is reported in ros2/rclpy#1194.
You’ll find three other suggestions online that I’d skip:
update-alternatives --config python3switches the system-widepython3, which Ubuntu’s own tools rely on.- Building rclpy from source on top of an apt install failed at the build step for both the Stack Exchange asker and the reporter of rclpy#1194.
- Running
/opt/ros/humble/bin/python3: there was no such file in the Jazzy image (check L), and no ROS doc I read says the ROS packages ship their own interpreter.
Diagnose one terminal
Run these in the terminal that fails, from top to bottom:
readlink /proc/$$/exe # the shell you're actually in
printenv ROS_DISTRO # "humble" if sourcing worked
printenv PYTHONPATH | tr ':' '\n' | grep /opt/ros # empty: first error, source issue
command -v python3 # a conda path: suspect the second error
python3 -c 'import sys, sysconfig; print(sys.executable, sys.version.split()[0], sysconfig.get_config_var("EXT_SUFFIX"))'
ls /opt/ros/humble/lib/python3.10/site-packages/rclpy/ | grep _rclpy_pybind11
python3 -c 'import rclpy'; echo $? # 0 means the import worked
If the EXT_SUFFIX on line 5 and the cpython-XY tag in the file name on line 6 differ, you have the interpreter problem. These lines read the same values the experiment read (current shell, PYTHONPATH, EXT_SUFFIX, file names, import exit code), but I haven’t run this exact block on Humble. On Jazzy, replace humble with jazzy and python3.10 with python3.12.
If you’ve got several terminals open, make a quick table: for each one, note whether it’s been sourced, which shell it runs, which setup file it used and where python3 points. A terminal only imports rclpy when all four are right. Sourcing alone isn’t enough.
Should the source line go in ~/.bashrc?
The docs suggest echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc (Configuring environment L95), and for interactive bash terminals that removes the problem. It doesn’t reach everything, though. Ubuntu’s default ~/.bashrc returns at the top when the shell isn’t interactive (I checked /etc/skel/.bashrc in the ubuntu:22.04 image). So bash script.sh, cron jobs and other shells never read that line, and they still need their own source. zsh users should put source /opt/ros/humble/setup.zsh in ~/.zshrc.
My take: if you use one shell and one ROS distro, put the line in your startup file. If you switch between distros or workspaces, or you also use conda, automatic sourcing hides which environment is active, and sourcing by hand in each terminal makes failures easier to explain. The official docs present both options without recommending either.
What this test doesn’t cover
- It ran on Jazzy, not Humble. The setup file templates are the same text in both. The Humble values in this article (Python 3.10,
cpython-310, Humble paths) come from docs and package metadata, not from a run. - No second interpreter. The container has a single Python 3.12, so the
_rclpy_pybind11error itself wasn’t reproduced. Only the file-name side (G, H) was checked. - No zsh. zsh with
setup.bashis a prediction from the template source. - One kind of interactive shell. C2 is an interactive dash on a pseudo-terminal. It doesn’t stand in for interactive bash, GUI terminal emulators, VS Code’s terminal or WSL2’s environment handling. Getting C2 right took three tries:
sh -i -c '…'and piping commands intosh -iboth discarded the rest of the input after the error, and only a pty with one line at a time behaved like a person typing. - No official listener test.
ros2 run demo_nodes_py listeneris an example forros-humble-desktopinstalls, and ros-base doesn’t include it (check L). If listener reports a missing package on ros-base, that isn’t an rclpy problem. Case J (init, create a node,shutdown) is a minimal stand-in, not the same test. - rpyutils’ other message, “failed to be imported while being present” (the file exists but won’t load), is out of scope.
- This is a software-environment check on a development machine. No robot or simulator was run, and the node in J doesn’t publish anything.
Sources and reproduction
The script, plan and raw results are in experiments/ros2-installed-rclpy-not-found-source-setup-shell (experiment.py, plan.md, results.json). To rerun it on an x86_64 Linux machine with Docker:
mkdir rclpy-shell-check && cd rclpy-shell-check
curl -fsSLO https://raw.githubusercontent.com/sungpyo9053/blog/33064efd9a3cf07c0676e1b8dc207f1fbdf8b8d5/experiments/ros2-installed-rclpy-not-found-source-setup-shell/experiment.py
sha256sum experiment.py # should be a8b509d6fb1fdf5dbf12b4c210a100a46b733a201ef64c2e76812d90299652c2
docker run --rm --network none --cpus 1 --memory 1g -v "$PWD":/work \
ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca \
bash -c "source /opt/ros/jazzy/setup.bash && python3 /work/experiment.py"
Each case prints one JSON line. If mismatched_cases in the final summary line is empty, your results match the predictions. Timings will vary from machine to machine. The harness also mounted /tmp as tmpfs and limited the process count, but the script starts every test shell itself in /tmp, so that shouldn’t affect the results.
- Robotics Stack Exchange 107736: Installed ROS2 Humble but rclpy not found (read through the Stack Exchange API on 2026-10-18)
- ROS 2 Humble: Ubuntu (deb packages) install, including per-shell setup files and the desktop-only talker/listener check
- ROS 2 Humble: Installation troubleshooting, covering interpreter mismatch and conda
- ROS 2 Humble: Configuring environment
- ament_package setup.bash template, setup.zsh template, setup.sh.in template
- rpyutils import_c_library.py, which produces the “isn’t present” message and its path
- PEP 3149, on version tags in extension file names
- POSIX Shell Command Language, 2.8.1, on interactive vs non-interactive error handling