Skip to main content

pyfitparsernative

Fast FIT file parser using Rust, exposed to Python via PyO3.

Reads and writes Garmin FIT files using the rustyfit Rust crate. Roughly 5-10x faster than garmin-fit-sdk (Python) for reading, and ~20-25x faster for writing.

Both benchmarks below (tests/test_performance.py) use the same bundled real ride file (81km / 11,555 records, ~22.6k total messages across all message types) — read it once, then write the parsed result straight back out:

Operation garmin-fit-sdk (Python) pyfitparsernative (Rust) Speedup
Read (parse_fit_file) ~1.1-2.2s ~0.1-0.2s ~5-10x
Write (write_fit_bytes) ~2.0-2.7s ~0.1s ~20-25x

Numbers vary by machine/container; both benchmarks measure through the full Python API (dict marshalling included), not raw Rust-only timings.

Installation

pip install pyfitparsernative

No Rust toolchain required — pre-built wheels are provided for Linux (glibc and musl), macOS (Intel and Apple Silicon), and Windows (x64). The macOS Intel (x86_64) wheel is cross-compiled and not tested in CI since GitHub no longer offers Intel macOS runners.

Usage

from pyfitparsernative import parse_fit_file, parse_fit_bytes, write_fit_file, write_fit_bytes

# Parse from a file path
messages = parse_fit_file("activity.fit")

# Parse from bytes
with open("activity.fit", "rb") as f:
    messages = parse_fit_bytes(f.read())

Return type: list[dict[str, Any]]

Returns a flat list of message dicts in original FIT file order. Each dict includes a "message_type" key (e.g. "record", "session", "lap") alongside the field data. Timestamps are returned as naive ISO 8601 strings (YYYY-MM-DDTHH:MM:SS) — see "Timestamps and timezones" below for what timezone that string is actually in.

# Example: get average power from session message
session = [m for m in messages if m["message_type"] == "session"][0]
print(session["avg_power"])    # 160
print(session["max_power"])    # 489

# Example: iterate record (data) messages
for msg in messages:
    if msg["message_type"] == "record":
        print(msg["timestamp"], msg.get("power"), msg.get("heart_rate"))

Writing

write_fit_file/write_fit_bytes accept the same list[dict[str, Any]] shape that parse_fit_file/parse_fit_bytes return, so parsed messages can be modified and written back directly. Each dict must have a "message_type" key; field values use the same units/representation as the parser (naive ISO 8601 strings for timestamps, unscaled physical values for numeric fields). Input is validated up front: unrecognized field names, values outside a field's FIT type range (e.g. heart_rate: 300), and messages with no fields all raise ValueError, and a failed write_fit_file never leaves a partial file behind.

from pyfitparsernative import write_fit_file, write_fit_bytes

messages = [
    {"message_type": "file_id", "type": "workout", "manufacturer": "garmin", "product": 0},
    {"message_type": "workout", "sport": "running", "num_valid_steps": 1, "wkt_name": "Easy Run"},
    {
        "message_type": "workout_step",
        "message_index": 0,
        "duration_type": "time",
        "duration_value": 1800000,  # ms
        "target_type": "speed",
        "intensity": "active",
    },
]

write_fit_file("workout.fit", messages)
data = write_fit_bytes(messages)  # -> bytes

Timestamps and timezones

Timestamps are never converted to the timezone of the machine running this code — the FIT file itself decides what you get back, not where you happen to be running the parser. Two different kinds of timestamp field appear in FIT files, and pyfitparsernative decodes each the same way regardless of runtime timezone:

  • date_time fields (record.timestamp, session.timestamp, session.start_time, activity.timestamp, ...) are stored in the FIT file as UTC. pyfitparsernative returns that UTC value as a naive ISO string — it looks like a local time (no +00:00/Z suffix) but the numbers are UTC.
  • local_date_time fields (currently just activity.local_timestamp) are stored by the recording device as its own local wall-clock time at the point of recording — the device bakes in whatever UTC offset it had configured, before writing the value. pyfitparsernative decodes this the same way it decodes date_time fields (no additional offset applied), so what comes back is the activity's original recording-location time, unrelated to wherever the code runs.

Concretely, for the bundled sample file (recorded at a UTC+1 offset), decoded on a machine in any timezone:

record[0]["timestamp"]           # "2026-01-31T13:41:14"  <- UTC
activity[0]["timestamp"]         # "2026-01-31T16:29:15"  <- UTC (same instant as above, later in the ride)
activity[0]["local_timestamp"]   # "2026-01-31T17:29:15"  <- the recording device's own local time (UTC+1)

activity[0]["timestamp"] and activity[0]["local_timestamp"] describe the same instant; their difference (here, 1 hour) is the recording location's UTC offset at that time, as the device had it configured — pyfitparsernative doesn't compute or expose that offset itself, only the two raw values.

Differences from garmin-fit-sdk

pyfitparsernative returns the same field names and numeric values as garmin-fit-sdk, with the following differences:

  • Timestamps — returned as naive ISO 8601 strings instead of timezone-aware datetime objects (see "Timestamps and timezones" above for what timezone the string is actually in — it's never the runtime machine's). Exception: garmin-fit-sdk only applies this conversion to date_time fields, not local_date_time fields (e.g. activity.local_timestamp), which it leaves as a raw integer; pyfitparsernative converts both to ISO strings.
  • Enum fields — resolved to the same string labels as garmin-fit-sdk (e.g. session["sport"] == "cycling", not 2), using the same FIT SDK profile data. A raw integer with no defined label (an unassigned/reserved value, or a manufacturer-specific extension) is left as an int, matching garmin-fit-sdk's own fallback. Writing accepts either form back: the string label or the raw int. Two message types are exempt — field_description and developer_data_id — since their fields describe other fields' types (e.g. field_description.fit_base_type_id) rather than holding values of those types; garmin-fit-sdk leaves these as raw ints too.
  • Message types — no _mesgs suffix (e.g. "session" not "session_mesgs").
  • Developer fields — garmin-fit-sdk uses integer dict keys for developer fields (e.g. {61: 2554}); pyfitparsernative uses string keys ({"developer_field_61": 2554}). Native fields missing from the FIT profile use a separate "unknown_field_<N>" prefix, so the two numbering spaces never collide. When writing, developer_field_<N> keys are re-encoded as real developer fields; the message list must contain the defining field_description message (with developer_data_index, field_definition_number, and fit_base_type_id) before the first message that uses the field — parsed files satisfy this automatically.
  • Component field expansion — garmin-fit-sdk derives speed and altitude from the more precise enhanced_speed/enhanced_altitude fields when the base fields are absent from the message definition. pyfitparsernative applies the same expansion, so "speed" and "altitude" are always present in record messages alongside their enhanced_ counterparts.
  • Sub-field expansion — not implemented. garmin-fit-sdk renames some fields based on a sibling field's value (e.g. file_id's generic product field becomes garmin_product when manufacturer == garmin; event's generic data field becomes rider_position for certain event types). pyfitparsernative always exposes the generic field name with its raw/enum-resolved value; the sub-field name is not produced.
  • Unknown message types — message numbers not in rustyfit's generated FIT profile fall back to their raw numeric string (e.g. "147") instead of a name.

Building from source

The docker/ directory contains scripts to build and test inside containers, avoiding the need for a local Rust toolchain. These are for local development only; CI/CD uses its own pipeline.

Build — compiles the wheel inside rust:1.93.1-trixie and writes it to dist/:

bash docker/docker-build.sh

Test — runs the test suite inside python:3.14.3-trixie against the wheel in dist/:

bash docker/docker-test.sh

On Windows, run these from Git Bash or WSL. Docker Desktop must be running with Linux containers enabled.

Releasing

Push a tag to trigger the CI/CD pipeline, which builds wheels for all platforms and publishes to PyPI:

git tag v0.1.0 && git push --tags

See .github/workflows/CI.yml for the full build matrix.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyfitparsernative-0.2.2.tar.gz (438.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyfitparsernative-0.2.2-cp39-abi3-win_amd64.whl (308.9 kB view details)

Uploaded CPython 3.9+Windows x86-64

pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_x86_64.whl (633.4 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_aarch64.whl (585.6 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (429.6 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (408.5 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

pyfitparsernative-0.2.2-cp39-abi3-macosx_11_0_arm64.whl (371.0 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

pyfitparsernative-0.2.2-cp39-abi3-macosx_10_12_x86_64.whl (377.8 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file pyfitparsernative-0.2.2.tar.gz.

File metadata

  • Download URL: pyfitparsernative-0.2.2.tar.gz
  • Upload date:
  • Size: 438.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyfitparsernative-0.2.2.tar.gz
Algorithm Hash digest
SHA256 60ae1d5eb853e8bf538568390f5df977e60fac3cbb7ea2f09b7abf82eca94d67
MD5 c99330b16e8d8c17a859d79f316580ff
BLAKE2b-256 b147f24d8f68d58752de9fdc40ba8a1374352111de89ce15774f3318cf94dcff

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2.tar.gz:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 52b30bc0409f8191fba2d1d22d871b04176cdcaa1e7408f156c29d2257e5c8f7
MD5 94617b29c9f6d20dc036dffb961a5ab1
BLAKE2b-256 aa3b51405bcf9d8842ce47e2853339244e9e327e4e86bc72734fa6bb8b4d0705

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-win_amd64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 66aa6db87e79dac7730689e927498ce32a9d4640831aa1bdcfb05ff41a1cc920
MD5 a62f5ff1cb0b591aa2e8a10b141c376a
BLAKE2b-256 94265670e48a2699f32a7372591a3574f6ce22543f7df96f7131d08392233eba

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_x86_64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 d974ef9b637acea54cbe529d168a97675fab368a85512a607840612e67d44b12
MD5 a49e98d4853e8cfbf3f6e7c7f0fb3964
BLAKE2b-256 c3102dfa1f4ec1f74b082e88845e01336efd0df42c767c3fd6eb6eafab19e0d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-musllinux_1_2_aarch64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5eaea714e24e836185377f78ec9214a9f2d6f074ca7e795b37227bad7b5d1b10
MD5 d12f7717224de7fa540bb4dc34089478
BLAKE2b-256 6d5bfda64110b546c30c907b010645bb6bc7ce7636f12090ea23e0d7364f5fd2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 bb08fd6376bb8cce3abb3d85adea5a97955aab7eac70f118427729502bfcd69c
MD5 0c804bb1a48ccb61bfd6783fd426bd99
BLAKE2b-256 625b06a4ede976ac08aa9f87c3e0978ded08a1839eda8699df23b1a5b86d7164

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 598ee6e5fb2f3c74964bfb4adc81018a3a3592e38caddbd2c6cf5aa90d8a0532
MD5 324f3c3d4151b78c96a32f87b96bee4a
BLAKE2b-256 9b910ce64be925ca5f22e7f8aa57d6a5c355b44e0d931bf32e4f6504300b5ba4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyfitparsernative-0.2.2-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pyfitparsernative-0.2.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0bc80176e9ff7383b47799175ff122c91266aeef09e21f4cea41e28abc89d5a1
MD5 217bf64192b1e69ad6ffd0167e8ed15e
BLAKE2b-256 414a651335b9367af25a6c4d4887da1f3653b5f3e9417d51a90562ead4e4814e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfitparsernative-0.2.2-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: CI.yml on GedRap/pyfitparsernative

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.3

8 files

This release

0.2.2 This release

8 files

0.2.1

8 files

0.2.0

8 files

0.1.0

10 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page