# python-udsoncan: speaking UDS to an ECU from Python

> python-udsoncan is a Python 3 implementation of the Unified Diagnostic Services protocol defined by ISO-14229, the request and response layer an automotive tool needs to talk to an engine control unit. It suits engineers building diagnostic tools or test benches, and it does not handle the CAN transport or the ISO-TP framing for you.

**pylessard/python-udsoncan** — Python implementation of UDS (ISO-14229) standard.

- Repository: https://github.com/pylessard/python-udsoncan
- Stars: 735 · Forks: 230
- Language: Python
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/pylessard-python-udsoncan

## A protocol layer, not a stack

python-udsoncan is an implementation of the Unified Diagnostic Services protocol defined by ISO-14229, written in Python 3 and published under the MIT licence. The package description is precise about the scope: it is the UDS service layer used in the automotive industry, and the topics it carries make the boundaries clear, with 14229, 15765, iso-tp, can, can-bus, obd2 and vehicle-diagnostic all listed.

That list is the map of the stack. ISO-14229 defines the services, which is what this library implements. ISO-15765 defines how those services are carried over CAN, which is the transport layer. The physical CAN bus and the socket or interface underneath it are further down still. The documentation is hosted at udsoncan.readthedocs.io, and the source lives in the udsoncan/ package.

The distinction matters more than it might appear. If you are writing a diagnostic bench, a fleet tool, or an automated test against an ECU, you need the service semantics: what a session change request looks like, what a negative response means, how a data identifier is encoded and decoded. Writing those payload bytes by hand is where the bugs live. This library exists so you do not have to.

If you only want to read a P-code from an OBD-II-equipped car, this is more than you need, and the project's own description of its audience, developers, and its classification as an interface engine or protocol translator point to a professional tooling context rather than a consumer one.

## The client API is built around a configuration object

The example in the readme is worth reading closely, because it shows the shape of the whole API. A connection is created, a Client is constructed with a request timeout and a configuration, and the client is used as a context manager so the session is torn down at the end of the block.

The configuration is the key idea. The example imports a fictive car library and passes MyCar.config into the client. Every parameter that depends on the vehicle, such as the security access level, the diagnostic session and the data identifier codecs, comes from there. The documentation even comments the VIN case: the standard identifier for the VIN is 0xF190, and the codec is set in the client configuration. The library does not assume a particular ECU layout.

The services are exposed as methods, and the names follow the standard closely. There is change_session with the DiagnosticSessionControl session enumeration, where the extended diagnostic session is the integer value 3. There is `unlock_security_access` taking the security level, which in the example is a fictive value of 5 coming from the fictive library. There is write_data_by_identifier taking a DataIdentifier and a value, with udsoncan.DataIdentifier.VIN as the standard identifier. And there is ecu_reset taking ECUReset.ResetType, where a hard reset is 0x01.

So the API is a direct transcription of the standard, with the vehicle-specific parts factored out into configuration. That is a good design for a protocol library, and it is also why the configuration is the first thing you will write.

## Negative responses are exceptions, and invalid payloads are a different exception

Error handling is where protocol libraries earn their keep, and the example distinguishes the two cases.

A NegativeResponseException means the server understood the request and refused it. The exception carries the response object, and the example prints the service name, the code name and the code in hex, so a tool can report that the server refused the request for service DiagnosticSessionControl with a given code. That distinction is real: a refusal carries meaning, a malformed answer does not.

InvalidResponseException and UnexpectedResponseException are grouped together in the example, and they mean the server sent something that does not fit the expected shape. The handler prints e.response.original_payload, the bytes as they arrived. For a diagnostic tool, that raw payload is the thing you want to see, because it is what tells you whether you are talking to the wrong ECU, a misconfigured codec, or a device that is not implementing the standard the way you expected.

The exceptions come from udsoncan.exceptions, and the services from udsoncan.services, both imported with a wildcard in the example. A Client also has a request_timeout parameter, set to 2 seconds in the example, which is the timeout that turns an ECU that does not answer into a failure rather than a hang.

The pattern in the example is worth copying as-is: wrap the sequence in try and except, handle the refusal and the malformed answer separately, and print the original payload when the answer is malformed.

## The transport is a separate library you have to bring

The example opens an IsoTPSocketConnection to the can0 interface with an isotp.Address, using Normal_11bits addressing with separate receive and transmit identifiers. The isotp module is imported as its own dependency, and the connection class comes from udsoncan.connections.

That split is the practical detail to plan for. python-udsoncan does not implement ISO-TP framing and does not talk to a CAN controller. It expects a connection object that implements the transport, and the repo's requirements.txt lists a single dependency, aioisotp, installed straight from its GitHub repository rather than from PyPI. The ISO-TP stack is a peer project that this library is designed to sit on top of.

The connection type also implies the deployment shape. A socket connection to can0 is a Linux box with a CAN interface, and the setup.py classifiers list POSIX and Linux as the operating system. The Dockerfile confirms the environment: it is based on ubuntu:20.04, and it builds CPython from source for several versions, 3.10.16, 3.11.11, 3.12.9 and 3.13.2, which is how a matrix of supported interpreters gets tested in one image.

There is also a scripts/ directory, a Jenkinsfile, a .travis.yml, a .coveragerc and a test/ directory in the tree, and the setup.py declares extras for test and dev, with mypy, coverage, ipdb and autopep8 in the dev set.

## Installing it and running the first session

The requirement is Python 3.7 or later, and the install is a single pip command:

```bash
    pip install udsoncan
```

The indentation there is the project documentation's own formatting, which is worth keeping in mind when you copy from it.

The full example from the documentation is the natural first thing to run, and it is short enough to read in one go:

```python
   import SomeLib.SomeCar.SomeModel as MyCar

   import udsoncan
   import isotp
   from udsoncan.connections import IsoTPSocketConnection
   from udsoncan.client import Client
   from udsoncan.exceptions import *
   from udsoncan.services import *

   udsoncan.setup_logging()

   conn = IsoTPSocketConnection('can0', isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x123, txid=0x456))
   with Client(conn,  request_timeout=2, config=MyCar.config) as client:
      try:
         client.change_session(DiagnosticSessionControl.Session.extendedDiagnosticSession)  # integer with value of 3
         client.unlock_security_access(MyCar.debug_level)   # Fictive security level. Integer coming from fictive lib, let's say its value is 5
         client.write_data_by_identifier(udsoncan.DataIdentifier.VIN, 'ABC123456789')       # Standard ID for VIN is 0xF190. Codec is set in the client configuration
         print('Vehicle Identification Number successfully changed.')
         client.ecu_reset(ECUReset.ResetType.hardReset)  # HardReset = 0x01
      except NegativeResponseException as e:
         print('Server refused our request for service %s with code "%s" (0x%02x)' % (e.response.service.get_name(), e.response.code_name, e.response.code))
      except (InvalidResponseException, UnexpectedResponseException) as e:
         print('Server sent an invalid payload : %s' % e.response.original_payload)
```

The first line is a placeholder: SomeLib.SomeCar.SomeModel is where your own vehicle definition and its configuration go. The identifiers 0x123 and 0x456 are the receive and transmit CAN identifiers for the ECU you are addressing, and they are the values you change first. The sequence itself is the canonical diagnostic opening: open the extended diagnostic session, authorise the security level, write a data identifier, then reset the ECU, with each failure mode handled separately.

## What this library does not do for you

The first limit is the one already noted and worth restating: the transport is not included. If you have a CAN interface but no ISO-TP implementation, this library cannot talk to anything. The reverse is also true, an ISO-TP stack on its own gives you framed messages, not services.

The second limit is the size of the standard. ISO-14229 defines a large service set, and the library implements the parts an automotive engineer needs day to day, but the topics and the example show a specific focus on session control, security access, data identifiers and reset. Work that needs the more unusual services, or work against an ECU with a non-standard implementation, means reading the service classes in the udsoncan/services package rather than assuming coverage.

The third is the configuration burden. The library deliberately does not know your ECU. Every data identifier codec, every security level, every timing parameter comes from a configuration object that you write against the vehicle's specification. That is the right default for a tool that has to work across many ECUs, but it means the work is moved rather than removed, and for a one-off script it can feel like a lot of scaffolding.

The fourth is platform shape. The classifiers name POSIX and Linux, the Dockerfile is Ubuntu based, and the example transport is a SocketCAN connection to can0. Running the whole stack on Windows or macOS is not what the project is set up for, and a hardware interface that is not SocketCAN would need a different connection implementation on your side.

## Maintenance, versioning and the licence

The project is not archived, and the last push was on 2026-09-21. The release history is close behind that: v1.26.0 on 2026-06-09, v1.26.1 on 2026-06-25, and before it v1.25.2 from 2026-02-13. The patch cadence of roughly a month and the fact that the branch has moved since the last tag together suggest a project in active use with fixes landing between releases.

The packaging is a plain setup.py with setuptools, declaring name udsoncan and taking the version from the package itself, udsoncan.__version__, so the version has a single source. It requires Python 3.7 or later, and it ships type information, since the package_data includes py.typed in the udsoncan package, plus any .conf files. The download URL is constructed from the version, which means a source tarball is published per release.

The licence is MIT, stated in setup.py, in the project readme and in the LICENSE.txt file at the root. For a protocol implementation that a company might embed in a commercial diagnostic product, that is the permissive end of the spectrum, and the obligations amount to keeping the notice.

The author is Pier-Yves Lessard, and the project describes its development status as 4 - Beta in the setup.py classifiers, which is worth noting against the maturity of the protocol it implements: the standard is decades old and settled, while this particular implementation of it is labelled as still maturing.

## Conclusion

python-udsoncan is the right library for a Python engineer who needs to talk UDS to a real ECU, whether for a bench, a vehicle diagnostic tool or a test suite, because the service semantics are implemented for you and the request and response handling is a matter of a configuration object rather than hand-built payload bytes. It is the wrong tool if you only need OBD-II requests, and it is the wrong tool if you are starting from raw CAN frames with no idea of the transport. Before you build on it, check the three things that decide the outcome: that you have a separate ISO-TP implementation, since the transport is a dependency rather than part of this package; that your ECU's data identifiers and security levels are covered by the configuration you write, because the example uses a fictive library for exactly that part; and that you are on a recent enough release, since the current version is 1.26.1 from 2026-06-25 while the last push was on 2026-09-21, so the branch is ahead of the last tag.

## FAQ

### How do I install python-udsoncan and connect to an ECU?

Install it with pip install udsoncan, which requires Python 3.7 or later. Connecting needs a separate ISO-TP transport: the example uses IsoTPSocketConnection from udsoncan.connections with the isotp module and a SocketCAN interface named can0, and the requirements list aioisotp from its GitHub repository.

### What is a UDS configuration object in python-udsoncan?

It is where the vehicle-specific values live. The client is constructed with a config argument, and it supplies the data identifier codecs, the security access level and the timing parameters. The example passes MyCar.config from a fictive car library, and notes that the VIN codec is set in the client configuration.

### How does python-udsoncan report an error from the ECU?

A refusal raises NegativeResponseException, which carries the service name, the code name and the code, so a tool can report that the server refused the request. A malformed or unexpected answer raises InvalidResponseException or UnexpectedResponseException, and the example prints e.response.original_payload to show the bytes that arrived.

## Sources

- [Issues](https://github.com/pylessard/python-udsoncan/issues)
- [License: MIT](https://github.com/pylessard/python-udsoncan/blob/master/LICENSE)
- [pylessard/python-udsoncan on GitHub](https://github.com/pylessard/python-udsoncan)
- [README](https://github.com/pylessard/python-udsoncan/blob/master/README.md)
- [Releases](https://github.com/pylessard/python-udsoncan/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pylessard-python-udsoncan
