A traceback appears when a VPN management command starts, and the first instinct runs to installing a different Python or pointing the system default somewhere else. That instinct mixes two runtimes that have nothing to do with each other. One belongs to the operating system and its packaged tools; the other belongs to whatever project you are building. Changing the first to satisfy the second is how a working desktop loses its package manager.

The comparison worth making is between the component that actually failed and the runtime you are entitled to change. They are almost never the same object.

Two Python questions that look like one

The first question is: which interpreter ran the program that printed the traceback? The second is: which interpreter should my project use? They look identical in a terminal because both are answered by typing python3, and they have different owners, different upgrade paths and different blast radii.

A system tool is bound to the interpreter its distribution shipped with, together with the compiled modules built against that interpreter's ABI. A project is bound to whatever environment you create for it. Moving the system default changes the first for every tool on the machine at once; creating an environment changes the second for one directory.

Only one of those actions is reversible in a minute. It is not the first one.

Which component produced the error

Read the first line of the traceback and the executable that produced it before reading anything else. The command-line frontend, the privileged service and the API bindings are separate pieces, and only some of them are Python at all.

command -v openvpn3
head -n 1 "$(command -v openvpn3)"
dpkg -S "$(command -v openvpn3)" 2>/dev/null
systemctl status openvpn3-service-client --no-pager

The first line of a script names the interpreter it will use, and that is the interpreter the traceback came from. The package query tells you who owns the file, which decides who is allowed to change it. The service status answers a different question entirely: whether the privileged backend that actually establishes sessions is running.

A module import error, a D-Bus activation refusal and a rejected credential are three different stages. An import error happens before any session exists, so no amount of VPN configuration will fix it, and a working service will not fix it either. Sorting the message into one of those three stages is the actual first step.

What a project environment can and cannot fix

A project environment solves a project problem: the exact library versions one application needs, isolated from everything else. That is genuinely useful, and it is the right tool when the failing program is a script you own.

uv run --python 3.11 app.py
python3 -c 'import openvpn3; print(openvpn3.__file__)'

The second command shows which copy of the bindings a given interpreter can see, which is the fastest way to tell a missing package from a shadowed one. Run it with the interpreter the failing script uses, not with a different one that happens to work.

What a project environment cannot do is repair a distribution-packaged frontend. That program's dependencies were built for the interpreter the package manager chose, and running it inside your project's environment either fails or produces a mixture nobody supports. Injecting paths into it with an environment variable is the same mistake with extra steps: you have changed what the tool loads without changing what it was tested against. That trade looks like a fix for exactly as long as it takes to hit the next import.

Why repointing the system interpreter is a trap

Changing the default interpreter is attractive because it appears to fix the traceback in one command. It also breaks the assumption every packaged Python tool on the host was built on, and the failures appear later and in unrelated places.

Alternative-selection mechanisms were designed to switch between equivalent implementations, not to install a newer language under older compiled modules. Compiled extensions are built for a specific ABI, so a module that imported yesterday can fail to load today without a single package being removed.

There is also a naming trap in the advice people find. The unversioned python command does not have one universal meaning across distributions and environments, and treating it as a fixed synonym for a particular major version produces fixes that only work on the machine where they were written. Read the policy of the system in front of you rather than the one in the blog post.

Separate acceptance tests

Two tests belong at the end, and passing one says nothing about the other. The first is that the supported client command works with the interpreter it was packaged for. The second is that the privileged service establishes a session and the tunnel carries traffic.

openvpn3 sessions-list
openvpn3-service-client --help 2>/dev/null | head -n 3
ip -4 addr show tun0

Keep an independent way onto the machine while you work, especially if the VPN carries your only administrative access. A change to a system interpreter is exactly the kind of edit that can remove that access halfway through.

If the failure is a missing module for a system tool, the correction is a package operation through the supported workflow. If the failure is a version conflict in your own project, the correction is an environment. Choosing between those two is the whole decision, and it is decided by which program printed the traceback.

Barruulee wal qabatan