wfdb-x for Python (the wfdbx package) is a faithful Python implementation
of the WFDB library and command-line tools (WFDB Software Package 10.7.0). It
reads and writes the same files as the original C library, and its tools
produce byte-identical output. See the project README for the
overall design.
It offers three interfaces:
-
the command-line tools (
rdsamp,rdann,wrsamp, ...), run through thewfdbxcommand; -
a high-level API that reads and writes whole records and annotation files as NumPy arrays (
read_record,read_annotations, ...); -
the C library API, with every function of the WFDB library under its C name (
isigopen,getvec,getann, ...). -
Pure Python; the only dependency is NumPy (used for fast bulk reading and by the high-level API).
-
Python 3.9 or later; Linux, macOS and Windows.
From PyPI:
pip install wfdb-x # the package and the wfdbx command
pip install "wfdb-x[examples]" # also matplotlib and Jupyter, for the notebooksFrom a copy of the repository (the Python package is its python/
directory):
pip install ./python # or: pip install -e ./python (development)The PyPI project is named wfdb-x; the package it installs is imported as
wfdbx, and its command is wfdbx.
The package includes example Jupyter notebooks, each self-contained and runnable without a network connection:
| Notebook | Contents |
|---|---|
01_getting_started |
the WFDB path; reading headers and signals; ADC and physical units |
02_annotations_and_heart_rate |
reading annotations; RR intervals and heart rate |
03_writing_records |
writing records and annotations, with explicit signal parameters |
04_command_line_tools |
running the tools (rdsamp, rdann, wrsamp, ...) |
05_c_library_api |
the C-style API (isigopen, getvec, getann, ...) |
06_physionet_records |
downloading a record from PhysioNet (with an offline fallback) |
07_legacy_mode |
the original package's assumptions, on request |
Copy them to a directory of yours and open them:
wfdbx --examples my-examples # or: python -c "import wfdbx; wfdbx.copy_examples('my-examples')"
cd my-examples
jupyter lab # or: jupyter notebookThey use the sample record 100s, also included in the package: the first
minute of record 100 of the
MIT-BIH Arrhythmia Database (two ECG
signals at 360 Hz, with its beat annotations). wfdbx.sample_data_dir()
gives its directory; add it to the WFDB path to use it:
import wfdbx
wfdbx.setwfdb(". " + wfdbx.sample_data_dir())
rec = wfdbx.read_record("100s", physical=True)or, for the tools, export WFDB=". $(python -c 'import wfdbx; print(wfdbx.sample_data_dir())')".
All tools are run through the wfdbx command, or as Python modules:
wfdbx rdsamp -r 100s -t 10 -v -p # same as the C 'rdsamp'
wfdbx --examples my-examples # copy the example notebooks
python -m wfdbx.apps.rdann -r 100s -a atr
wfdbx --list # list the available tools
wfdbx --versionOptions, output, error messages and exit statuses are those of the original
tools, documented in the WFDB Applications Guide
(third_party/wag.pdf). Available tools:
- signals:
rdsamp,wrsamp,wfdbdesc,sigamp,snip,xform,sampfreq,signame,signum,wfdbcat,wfdbwhich - time conversion:
wfdbtime,time2sec - annotations:
rdann,wrann,sortann,ann2rr,rr2ann - converters:
edf2mit,mit2edf,rdedfann,wav2mit,mit2wav,wfdb2mat,a2m,m2a,ad2m,md2a,ahaecg2mit,makeid,readid,revise,parsescp
The tools never assume a sampling frequency, a gain, an ADC resolution or units that a record or the user did not give; see No built-in signal parameters below.
Records are found using the WFDB path (the WFDB environment variable, as
with the C library; see getwfdb and setwfdb). Remote records can be read
when the path includes http:// or https:// prefixes.
The original package silently assumes a sampling frequency (250 Hz), a gain
(200 adu/mV), an ADC resolution (12 bits) and units (millivolts) whenever a
record or the user does not give them. These values describe the equipment
of the first PhysioNet databases, not modern recordings, and a silently
assumed value corrupts every time, duration, resampled signal and physical
value derived from it, with nothing in the output to show it. wfdb-x
therefore never assumes them: tools that write them require them, and an
operation that needs a value missing from a record stops with a clear error
instead of guessing. This deliberate divergence, the same in every wfdb-x
implementation, is described in
docs/DIVERGENCES.md.
New or now required options:
wrsamp: with-o,-F FREQ,-G GAINand the new-R BITS(ADC resolution) are required, and the new-U UNITSis required for calibrated signals unless the input's second line gives the units (-G,-Rand-Utake a quoted list, one value per signal, the last value applying to the remaining signals).wav2mit: new required-G GAIN(0: uncalibrated) and-U UNITS(unless the gain is 0); the frequency and resolution come from the WAV file.ad2mandahaecg2mit: new required-F,-G,-Rand-U(unless the gain is 0); the options ofahaecg2mitprecede the input files.a2m: new-F FREQ, used when the record header gives no frequency; annotation times in milliseconds are converted at the record's frequency (C assumed 4 ms per sample).rdann,ann2rr,sortannandm2a: new-F FREQ, used when the record header is missing or gives no frequency.mit2edfstops if the frequency, a gain or an ADC resolution is not specified;xformasks for an ADC resolution it cannot take from the input; unspecified units are shown asnd(byrdsamp -v,wfdbdesc,wfdb2mat,mit2edfandxform), and an unspecified ADC resolution as 0 (wfdbdesc: "not specified").
To reproduce the results of the original tools, give the values they assumed explicitly, for example:
wfdbx wrsamp -F 250 -G 200 -R 16 -U mV -o rec -i samples.txt
wfdbx ad2m -F 250 -G 400 -R 12 -U mV -i 1201.dat -r 1201
wfdbx ahaecg2mit -F 250 -G 400 -R 12 -U mV 1201.ecg
wfdbx wav2mit -G 6400 -U mV -i sound.wav # 16-bit; 12.5 for 8-bit files
wfdbx a2m -F 250 -i 1201.ANOLegacy mode. For compatibility, the original behavior remains available
on request. --legacy as the first argument of any tool (wfdbx rdsamp --legacy -r 100s -t 10) makes it behave exactly as the original tool (same
options, outputs, messages, help texts and exit statuses; the options added
by wfdb-x are then unrecognized). In the library, wfdbx_set_legacy(1)
turns legacy mode on (it returns the previous setting; wfdbx_get_legacy()
returns the current one), and the high-level functions take a legacy=False
keyword: legacy=True gives the behavior of the original package (for
example, write_record without gains writes 200 adu/mV and an ADC resolution
based on the format, and read_record(..., physical=True) assumes 200 adu/mV
for uncalibrated signals and 250 Hz for a header without a frequency), with
legacy mode on for the duration of the call. Legacy mode reproduces the
original package exactly, and is tested against its results (the legacy
conformance suite, harness.py check --legacy). It is kept for
compatibility only: the values it assumes are exactly the silent defaults
this project removed, so results produced with it carry the same risks.
import wfdbx
rec = wfdbx.read_record("100s", sampfrom="0:10", sampto="0:20", physical=True)
rec.signals # NumPy array, one column per signal (mV here)
rec.fs, rec.names, rec.units, rec.base_time, rec.comments
ann = wfdbx.read_annotations("100s", "atr")
ann.samples, ann.symbols, ann.subtypes, ann.chans, ann.nums, ann.aux
wfdbx.write_record("new", rec.signals, fs=360, fmt=212, physical=True,
gains=200, adcres=11, units="mV", names=["MLII", "V5"])
wfdbx.write_annotations("new", "atr", ann.samples, ann.symbols)| Function | Purpose |
|---|---|
read_record(record, channels=None, sampfrom=0, sampto=None, physical=False, highres=False, pad=False) |
Read signals into a Record (signals array of shape (samples, signals), plus fs, names, units, gains, baselines, base_time, comments, siginfo). |
read_annotations(record, annotator, sampfrom=0, sampto=None, fs=None) |
Read an annotation file into an Annotations object (samples, codes, symbols, subtypes, chans, nums, aux, fs). |
read_header(record) |
Return the list of Siginfo objects (signal specifications) of a record, without reading any samples. |
write_record(record, signals, fs, fmt=16, gains=..., adcres=..., units=..., ...) |
Write a header and a signal file (record.dat) from an array (gains, adcres and the units of calibrated signals are required). |
write_annotations(record, annotator, samples, symbols=None, codes=None, ...) |
Write an annotation file. |
Notes:
- Records are found in the WFDB path, as by the C tools. Times (
sampfrom,sampto) are sample numbers or WFDB time strings such as"0:10"(10 s),"s3600"(sample 3600) or"e"(end of record);samptois exclusive. read_recordalso acceptschannels(numbers or names),highres=True(multi-frequency records at the highest sampling frequency), andpad=True(replace invalid samples by the previous valid one). Invalid samples arewfdbx.WFDB_INVALID_SAMPLE(or NaN in physical units).- No sampling frequency, gain, ADC resolution or units are ever assumed (see
docs/DIVERGENCES.md):write_recordrequiresgains(a scalar or one value per signal); a gain of 0 marks a signal as uncalibrated, which is allowed only for samples in ADC units.fsmust be greater than 0.adcres(ADC resolution in bits, >= 1; none is inferred from the format) is required, and so are theunitsof every signal whose gain is not 0 (otherwiseValueError: "ADC resolution(s) must be specified" or "units must be specified").read_recordreports an unspecified ADC resolution as 0 (siginfo[k].adcres) and unspecified units asNone(never "mV").read_recordreturnsfs = 0for a record whose header gives no sampling frequency; time strings that need it (anything but sample numbers"s...","e","i"and"o"; seewfdbx.wfdbx_time_needs_freq) then raiseValueError, as doesphysical=Truefor a selected uncalibrated signal.read_annotationstakes an optionalfs, used (likerdann -F) when the record has no header or its header gives no frequency; without one, the result'sfsis 0 and time strings that need it raiseValueError.
- Errors raise
IOError(a record, header or annotation file cannot be read or written) orValueError(unknown channel, inconsistent arguments, missing sampling frequency, gain, ADC resolution or units). - These functions call
wfdbquit(), so they close anything opened with the C library API.
See help(wfdbx.read_record) and the other docstrings for all the arguments.
Every function of the WFDB library is available under its C name, with the
same arguments and return values (see help(wfdbx.isigopen), for example).
Where a C function fills an array supplied by the caller, the Python function
fills a list supplied by the caller:
import wfdbx as w
si = [] # list of w.Siginfo
nsig = w.isigopen("100s", si, 2) # opens 2 signals, fills si
v = [0] * nsig
while w.getvec(v) > 0: # v receives one sample per signal
...
print(w.timstr(w.strtim("e")), si[0].desc, si[0].gain)
a = w.Annotation()
w.annopen("100s", [w.Anninfo("atr", w.WFDB_READ)])
while w.getann(0, a) == 0:
print(a.time, w.annstr(a.anntyp), a.aux_string())
w.wfdbquit()Notes on the correspondence with C:
-
As in every wfdb-x implementation, the library assumes no sampling frequency or gain (
docs/DIVERGENCES.md):sampfreqreturns 0 for a header without a frequency; without a frequency,strtimfails (returns 0) for strings that need one,timstrandmstimstrreturn the time as a sample number ("s360"), andsetifreqfails;aduphys,physadu,adumuvandmuvadufail for an uncalibrated signal or an invalid signal number (NaN orWFDB_INVALID_SAMPLE).wfdbx_time_needs_freq(string)(a wfdb-x addition) tells whetherstrtimneeds the frequency for a string. -
Strings are
str; text and binary data are handled as ISO-8859-1, so any byte sequence round-trips unchanged. -
An annotation's
auxfield isNoneor abytesobject whose first byte is the length of the data that follows (as in C);Annotation.aux_string()returns it as astr. -
getframes(n)(not in the C library) reads up tonframes at once and returns(array, stat): a NumPy array with one row per frame read, and thegetvecstatus of the first frame that could not be read (or the number of signals if allnframes were read). It is exactly equivalent to up toncalls ofgetvec. -
getseginfo()returns the list ofSeginfoobjects of the open multi-segment record (C returns their number and an array). -
Functions that report errors in C by returning a negative code do the same here (they do not raise exceptions); the message is available from
wfdberror(), and is printed on the standard error output unlesswfdbquiet()was called. -
As in C, the library keeps global state (the open record, output signals, annotators, WFDB path, time base). Call
wfdbquit()to reset it.
Annotation codes (NORMAL, PVC, ...) and the mapping functions of
<wfdb/ecgmap.h> (isqrs, map1, map2, ammap, mamap, annpos, ...)
are also provided.
Formats 508, 516 and 524 are supported without any external library. Files written by the C library (libFLAC) are read exactly. Files written by wfdb-x are standard FLAC streams that the C library reads back with identical sample values; their compressed bytes differ from libFLAC's (whose output depends on the libFLAC version and the processor).
Sequential reading uses a vectorized (NumPy) decoder that is exactly
equivalent to the sample-by-sample path. For example, rdsamp prints a
650,000-sample, 2-signal format 212 record in about 0.9 s.
From the root of the repository:
pip install -e "./python[test]"
pytest python/testsThe notebook tests execute every example notebook (they need the test
extra: nbformat, nbclient and ipykernel).
This runs the unit tests and the cross-language conformance suite
(tests/conformance/) against the Python tools. Set WFDBX_CONFORMANCE=0 to
skip the latter, or e.g. WFDBX_CONFORMANCE=rdsamp,rdann to run part of it.
In a source distribution, which does not include the repository's test
records and conformance suite, the tests that need them are skipped. The
conformance suite can also be run on its own, in parallel:
PYTHONPATH=python/src python3 tests/conformance/harness.py check --impl python -j8and its legacy suite (the original package's cases, run with --legacy)
with ... check --legacy --impl python -j8.
The WFDB library and tools were written by George B. Moody, with revisions by
Benjamin Moody and contributions from many others; parsescp was written by
George B. Moody and Edna S. Moody. The header of each module names the authors
of the C code it ports, and docs/AUTHORS.md gives the
details. The Python port was written by Reza Sameni (October 2026).
The library (wfdbx, except wfdbx.apps) is distributed under the GNU
Library General Public License, version 2 or later; the command-line tools
(wfdbx.apps) under the GNU General Public License, version 2 or later. See
LICENSE.LIB and LICENSE.