pyModeS 3 replaces per-field decoders with one decode() call
Python decoder for Mode S and ADS-B signals
At a glance
- What is it?
- pyModeS is a Python decoder for Mode S and ADS-B messages that was rewritten from the ground up for version 3, with a single unified decode() function, a stateful PipeDecoder for streams and a modes command line tool. It is not backwards compatible with the 2.x API, it has no runtime dependencies, and its meteorological Comm-B registers are heuristic and off by default.
- Who is it for?
- Adopt pyModeS 3 if you are decoding Mode S or ADS-B traffic in Python and want one function that returns every decodable field, plus a stateful decoder that resolves CPR pairs and verifies ICAO addresses across a live feed.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the v3 rewrite changed about the API
pyModeS is a decoder for Mode S and ADS-B messages in Python, and the headline of this version is that it was rewritten from the ground up. The README is blunt about the consequence: pyModeS 3 is not backwards-compatible with pyModeS 2.x.
The old API was a function per field, the kind where you call pms.adsb.callsign on a message and then pms.adsb.altitude on the same message and each call re-does the parsing work. The new API is a single decode() that returns a Decoded dict with every decodable field populated in one pass. That is a different shape of program, not just a different spelling, and it changes how a batch of messages should be handled.
The value of the rewrite is speed and coherence. A tracker that wants callsign, position, altitude and velocity for every aircraft now does one decode per message instead of four or five, and gets a dict it can filter on. The batch mode preserves list length, with errors becoming error dicts rather than exceptions, so a malformed message in the middle of a capture does not abort the run.
Two signals of engineering discipline in the repository back this up. The feature list says the source is type-checked under mypy strict across all source files, and there is a golden-file oracle regression test against pyModeS 2.21.1, which is how a rewrite can claim continuity with the behaviour of the version it replaces. The pyproject.toml marks the project as Development Status 4 - Beta and requires Python 3.11 or newer, with classifiers for 3.11, 3.12 and 3.13.
One call to decode a single message
The simplest use is a single message and one call. The example in the README decodes a DF17 identification message and prints the result:
from pyModeS import decode
result = decode("8D406B902015A678D4D220AA4BDA")
print(result)The returned dict carries the downlink format, the ICAO address, whether the CRC was valid, the typecode, the BDS register pair, the callsign, the aircraft category and the wake vortex category. The CRC result is worth noting on its own, because a message whose CRC does not check out should not be trusted, and having that flag in the same dict as the decoded fields means you can filter on it.
The API is deliberately flat. There is no object with methods, no session to open, no configuration object. You import decode and you call it.
There is also a mode for the other direction of work. The pyModeS.util module exposes thin wrappers around the bit, hex and CRC primitives for ad-hoc message inspection: hex2bin, bin2int, hex2int, bin2hex, crc, df, icao, typecode, altcode, idcode and cprNL. A CRC check returns zero for a valid DF17 message, and typecode returns the numeric ADS-B type. The README shows what that looks like in practice:
from pyModeS.util import hex2bin, crc, icao, typecode
msg = "8D406B902015A678D4D220AA4BDA"
hex2bin(msg)[:16] # '1000110101000000'
icao(msg) # '406B90'So a project can start at the raw bit level and move up to the full decode as needed, rather than committing to one level on day one.
Batch decoding and how CPR pairs get resolved
The batch mode is where the rewrite pays off, and the mechanism is worth understanding because position decoding in Mode S is inherently a two-message problem.
Compact Position Reporting transmits a position in a compressed form that is only unambiguous relative to a reference. Two messages, one even and one odd, are needed before a position can be resolved globally. The batch API therefore takes a list of hex strings and a parallel list of timestamps in seconds, and the dispatcher routes each message to the right decoder while using the timestamps to resolve CPR pairs and to disambiguate ambiguous Comm-B registers.
from pyModeS import decode
results = decode(
[
"8D406B902015A678D4D220AA4BDA", # DF17 BDS 0,8 identification
"8D485020994409940838175B284F", # DF17 BDS 0,9 airborne velocity
"8D40058B58C901375147EFD09357", # DF17 BDS 0,5 airborne pos (even)
"8D40058B58C904A87F402D3B8C59", # DF17 BDS 0,5 airborne pos (odd)
"A000178D10010080F50000D5893C", # DF20 BDS 1,0 data link capability
"A8000D9FA55A032DBFFC000D8123", # DF21 BDS 6,0 heading & speed
],
timestamps=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
)The example mixes downlink formats on purpose, DF17 alongside DF20 and DF21, and the assertions that follow check the properties that matter: the callsign from the identification message, the groundspeed from the velocity message, and a resolved latitude on the odd airborne position message.
Surface position is a separate case because it needs a reference close by. The README states that surface CPR needs a reference within about 45 nautical miles, and you can pass an ICAO airport code looked up in the shipped airport database, or an explicit latitude and longitude tuple:
from pyModeS import decode
# Real DF18 surface movement on LFBO (Toulouse-Blagnac).
r = decode("903a23ff426a4e65f7487a775d17", surface_ref="LFBO")
print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...The timestamps are not optional decoration. They are what makes CPR pairing possible, and a caller who supplies arbitrary values will get positions that are wrong rather than an error.
PipeDecoder holds state so a live feed does not need re-pairing
Batch mode is stateless, which is fine for a file of captures but wrong for a stream, because a stream decoder has to remember what it has already seen. PipeDecoder is that stateful component.
The README describes it as holding per-ICAO state across calls, matching CPR pairs automatically, evicting stale aircraft after a time to live, and flagging DF20 and DF21 messages as icao_verified when their CRC-derived ICAO address was seen recently in clean DF17 or DF18 plaintext.
That last capability is a nice piece of engineering. DF20 and DF21 are Comm-B replies whose ICAO address is not directly readable, so it has to be recovered by trying candidate addresses and checking which one produces a valid CRC. Doing that well requires knowing which addresses are actually in the air, and the trusted-set promotion approach means an address is only trusted once it has been seen in a message format where the address is unambiguous.
The usage is a loop with a timestamp:
from pyModeS import PipeDecoder
pipe = PipeDecoder(surface_ref="EHAM")
for msg, timestamp in stream:
decoded = pipe.decode(msg, timestamp=timestamp)
if decoded.get("latitude") is not None:
print(decoded["icao"], decoded["latitude"], decoded["longitude"])The README insists that timestamps come in seconds from a consistent clock. Time to live eviction and CPR pairing both depend on it, and a feed that mixes clock sources will produce intermittent garbage that is hard to trace back to the cause.
There is also a full_dict mode, where every key in the canonical 123-field schema is populated, which the README points at pandas and parquet workflows. That is a deliberate shape: one wide, stable schema is easier to load into a columnar tool than a variable set of keys.
The modes command covers one-shot decodes and live beast feeds
The package installs a console script called modes, and it covers both ad-hoc work and live streaming.
The decode subcommand handles a single message, several messages inline, a file of captures, and standard input. Inline messages are comma separated and the output is JSON lines, and a single message is pretty-printed JSON. The file mode accepts one hex message per line, or a timestamp and comma CSV, and a reference can be supplied for airborne CPR when decoding a single message:
modes decode 8D40058B58C901375147EFD09357 --reference 49.0 6.0modes decode --file captures/flight.logcat taxi.log | modes decode --file - --surface-ref LFBOThe live subcommand reads a dump1090-style beast feed over TCP, and Mode-S Beast binary format is supported for dump1090 and compatible feeds. Output is JSON lines, and it can be teed to a file with the dump-to option. There is also an interactive table view, which requires the optional tui extra:
pip install "pyModeS[tui]"
modes live --network host:10006 --tuiThat extra is the only runtime dependency in the whole project, and it is optional. The pyproject.toml says v3 has no runtime dependencies at all, because the decoder is pure Python on top of the standard library, using integer shifts, math, socket, json and argparse. The tui extra pulls in textual.
One piece of housekeeping is worth flagging for anyone upgrading. The v2 console script called modeslive is still registered in pyproject.toml, but only so that invoking it prints a migration hint pointing at modes live and modes decode, rather than a shell command not found error.
What the decoder covers and what it deliberately leaves out
The supported message list is explicit, which is the most useful thing a decoder documentation can do.
For surveillance replies, DF4 and DF20 carry the altitude code, DF5 and DF21 the identity code, and DF11 the all-call reply, which the README marks as partial because II and SI decoding is deferred. For ADS-B, DF17 and DF18 cover typecodes 1 to 4 as identification with category, 5 to 8 as surface position, 9 to 18 as airborne position with barometric altitude, typecode 19 as airborne velocity with all four subtypes, 20 to 22 as airborne position with GNSS altitude, 28 as aircraft status, 29 as target state and status, and 31 as operational status.
For Comm-B, DF20 and DF21 cover BDS 1,0 data link capability, 1,7 common-usage GICB capability, 2,0 aircraft identification, 3,0 ACAS active resolution advisory, 4,0 selected vertical intention, 4,4 meteorological routine air report, 4,5 meteorological hazard report, 5,0 track and turn report, and 6,0 heading and speed report.
Two limitations are called out rather than hidden. DF11 is only partially implemented. And BDS 4,4 and 4,5 are described as heuristic and disabled by default, which means you opt in with include_meteo=True on decode or on PipeDecoder when you know those registers are on your feed. A decoder that guesses at meteorological data without telling you it is guessing is worse than one that refuses, so the default being off is the right call.
There is a further ambiguity worth knowing about. Comm-B BDS 5,0 and 6,0 share register space with other data, so they cannot always be disambiguated from the message alone. The known= parameter takes an aircraft state dictionary to resolve it, which means the caller has to maintain some state of its own for those cases.
The licence is GPL-3.0, which matters if you intend to use this in a service you distribute rather than a research pipeline, since that is a copyleft obligation on redistribution and the classifier in pyproject.toml records it as the GNU General Public License v3.
Editorial conclusion
Adopt pyModeS 3 if you are decoding Mode S or ADS-B traffic in Python and want one function that returns every decodable field, plus a stateful decoder that resolves CPR pairs and verifies ICAO addresses across a live feed. Do not adopt it if you have existing 2.x code, because the API was replaced outright and the migration is not optional, and do not treat its BDS 4,4 and 4,5 meteorological output as authoritative, since the README documents those registers as heuristic and disabled by default. Verify first that your feed delivers timestamps in seconds from a consistent clock, because position decoding and Comm-B disambiguation depend on them, that your surface movement data has a nearby reference such as an ICAO airport code, and that the version you pin is 3.x, since v3.6.1 is the current release and the 2.x series has a different API entirely.
Frequently asked questions
How do I install pyModeS and what Python version do I need?
Install it with pip install "pyModeS>=3". The README states that Python 3.11 or higher is required, and the pyproject.toml declares requires-python of 3.11 or later with classifiers for 3.11, 3.12 and 3.13.
Is pyModeS 3 backwards compatible with pyModeS 2.x?
No. The README states that pyModeS 3 is not backwards-compatible with 2.x, because the function-per-field API such as pms.adsb.callsign is replaced by a single decode() returning a dict. The repository also keeps the old modeslive console script registered only to print a migration hint.
How do I decode a live ADS-B feed with pyModeS?
Use PipeDecoder, which holds per-ICAO state, matches CPR pairs automatically and evicts stale aircraft after a TTL, passing each message with a timestamp in seconds from a consistent clock. From the shell, the modes live subcommand reads a dump1090-style beast feed over TCP and emits JSON lines, with an optional --tui table that needs the pyModeS[tui] extra.
Why does pyModeS need timestamps to decode a position?
Compact Position Reporting is only unambiguous when an even and an odd message are paired, and the timestamps are what let the decoder resolve those CPR pairs and disambiguate ambiguous Comm-B registers. The README says to pass timestamps in seconds from a consistent clock, and the batch example supplies them as a parallel list.
Does pyModeS decode meteorological Comm-B data?
BDS 4,4 routine air report and 4,5 hazard report are supported but described as heuristic, so they are disabled by default. You enable them with decode(..., include_meteo=True) or PipeDecoder(include_meteo=True) when those registers are expected on the feed.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/junzis-pymodes)Was this useful?