Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Calendar Versioning.

The first number of the version is the year. The second number is incremented with each release, starting at 1 for each year. The third number is for emergencies when we need to start branches for older releases.

Unreleased

Fixed

  • Simplify short tracks and routes crossing the antimeridian using the local longitude displacement, so collinear points near ±180° are removed correctly while significant bends are preserved.

2026.4.0 - 2026-09-13

This fourth major release in the year 2026 adds GPX 1.1 schema validation and a public operations API for editing and merging GPX data, including new operations to split, simplify and smooth tracks. It also makes parsing and serializing GPX data ~20× faster, and includes a range of bug fixes and robustness improvements.

Added

  • New operations module that exposes the editing and merging operations behind the gpx edit and gpx merge CLI commands as a public, reusable API:

    • crop(): Crop a GPX to a geographic bounding box

    • trim(): Trim a GPX to a date/time range

    • reverse(): Reverse the routes and/or tracks of a GPX

    • strip_metadata(): Strip metadata (fields) from a GPX

    • reduce_precision(): Reduce the precision of the coordinates and/or elevations of a GPX

    • filter_points(): Filter the points of a GPX with an arbitrary predicate

    • merge(): Merge multiple GPX instances into one

    • split(): Split track segments at time and/or distance gaps

    • simplify(): Simplify the tracks and routes of a GPX with the Ramer-Douglas-Peucker algorithm

    • smooth(): Smooth the coordinates and/or elevations of the tracks and routes of a GPX with a moving average

    • shift_time(): Shift all point timestamps of a GPX by a time delta

    • strip_extensions(): Strip all extensions from a GPX

    • All operations are pure (they return a new GPX instance and never mutate the input) and are importable directly from the top-level package (e.g. from gpx import crop, merge).

  • New gpx edit CLI options exposing the new operations:

    • --split-time-gap SECONDS and --split-distance-gap METERS: Split track segments at gaps

    • --simplify TOLERANCE: Simplify tracks and routes (tolerance in metres)

    • --smooth WINDOW: Smooth track and route coordinates and elevations with a moving average

    • --shift-time SECONDS: Shift all point timestamps (may be negative)

    • --strip-extensions: Strip all extensions

  • New file conversion functions in the io module, exposing the operation behind the gpx convert CLI command as a public, reusable API:

    • convert_file(): Convert a file between the GPX, GeoJSON and KML file formats

    • detect_format(): Detect the file format from a file path’s extension

    • Both are importable directly from the top-level package (e.g. from gpx import convert_file).

  • New validation module that validates GPX data against the GPX 1.1 schema (gpx.xsd) without any extra dependencies:

    • validate(): Validate a file path, a string of GPX content, or a GPX instance and return a ValidationResult.

    • validate_text(): Validate a string that is always treated as GPX content (never as a file path), e.g. for untrusted input.

    • ValidationResult: Holds all issues, with is_valid, errors and warnings properties.

    • ValidationIssue: A single issue with a severity, message, path (e.g. gpx > trk[0] > trkseg[2] > trkpt[14]) and source line (when available).

    • Severity: Enum of ERROR and WARNING.

    • InvalidGPXError: Raised by strict parsing; carries the full ValidationResult.

    • Detects, among others: wrong root element / namespace (with a GPX 1.0 hint), missing required attributes, unknown elements (with “did you mean …?” suggestions), duplicate single-occurrence elements, out-of-order children, <extensions> children that are not in a foreign namespace (e.g. unprefixed elements that inherit the default GPX namespace), and invalid values (latitude/longitude/degrees ranges, fix, dgpsid, sat, copyright year, time).

    • All names are importable directly from the top-level package (e.g. from gpx import validate).

  • New strict keyword argument on read_gpx() and from_string(). When strict=True, the input is validated against the GPX 1.1 schema first and an InvalidGPXError is raised if any errors are found. The default (strict=False) keeps the existing lenient behavior.

  • New Year type (an int subclass) for xsd:gYear values such as 2004, 2004Z or 2004+02:00. The optional timezone is kept in Year.timezone, and str() returns valid xsd:gYear text. It’s importable from the top-level package (from gpx import Year).

  • New gpx validate CLI options:

    • --strict: Treat warnings as failures (non-zero exit code).

    • --json: Output a machine-readable validation report.

  • New --strict option on the gpx info, gpx edit, gpx merge and gpx convert CLI commands. When given, each input is validated against the GPX 1.1 schema first: warnings are printed to stderr and the command aborts (non-zero exit code) if any schema errors are found. For gpx convert, validation only applies to GPX input. The default keeps the existing lenient behavior.

Changed

  • The CLI (gpx edit, gpx merge and gpx convert) now uses the new operations module and io conversion functions internally (behavior is unchanged).

  • The gpx validate CLI command is now a real GPX 1.1 schema validator. It reports all errors and warnings (with source line numbers) instead of only checking whether the file can be parsed, and exits non-zero when errors are found (or, with --strict, when warnings are found).

  • Copyright.year is now typed as Year | None instead of int | None. Year is an int subclass, so existing code keeps working at runtime, but type checkers now expect Year(2004) instead of 2004.

  • Parsing and serializing GPX data is ~20× faster: the type annotations of each model are now resolved once per class instead of once per XML element.

Fixed

  • Writing a parsed GPX file no longer moves the document into the wrong XML namespace when an element deeper in the file (e.g. a Garmin extension) redeclares the default namespace, or when text content contains xmlns="...". Only the namespace declarations on the root element are now preserved.

  • __geo_interface__, write_geojson() and gpx convert to GeoJSON no longer crash on routes and tracks without any points. Their geometries are now written without a bbox.

  • Track.max_speed and Track.min_speed no longer raise when a track segment has fewer than two points. Such segments are now skipped.

  • NaN and Infinity values are now rejected with a ValueError by Latitude, Longitude and Degrees (instead of an unhelpful decimal.InvalidOperation). validate() now reports them as errors instead of crashing.

  • GeoJSON output (__geo_interface__, write_geojson() and gpx convert) no longer contains Python object representations (with memory addresses) for extensions. Extensions have no GeoJSON representation and are now left out of the properties.

  • GeoJSON Features created for waypoints, routes and tracks without any properties now have "properties": null, as required by RFC 7946. They previously had no properties member, which strict GeoJSON readers reject.

  • from_wkb() now correctly reads geometries with M values (ISO M/ZM types and EWKB M flag) and EWKB geometries with an embedded SRID (e.g. PostGIS ST_AsEWKB output). These previously produced wrong coordinates without any error.

  • from_string(..., strict=True) no longer treats content that doesn’t start with < as a file path to read. Such content is now reported as not well-formed XML.

  • read_kml() and gpx convert now read KML files in the legacy Google Earth namespaces (http://earth.google.com/kml/2.0, 2.1 and 2.2), which previously produced an empty GPX without any error. Documents whose root element is not <kml> in a supported namespace (or no namespace) now raise a ValueError.

  • read_gpx(), validate() and the CLI now decode GPX files according to the encoding in their XML declaration (e.g. ISO-8859-1). They previously always decoded files as UTF-8 and failed with a UnicodeDecodeError on other encodings. from_string() and validate_text() now also accept encoded bytes.

  • from_geo_interface(), read_geojson(), from_wkt() and from_wkb() now raise a ValueError for geometries without a GPX equivalent (e.g. Polygon), including ones nested in a FeatureCollection or GeometryCollection. They were previously dropped without any error, so e.g. from_wkt("POLYGON (...)") returned an empty GPX.

  • from_wkb(), from_wkt() and from_geo_interface() now raise a ValueError, as documented, for truncated WKB data and coordinates with fewer than two values. They previously raised struct.error or IndexError. An empty WKT point (POINT EMPTY) now produces no waypoint instead of raising.

  • Timestamps with sub-millisecond precision are no longer cut off at milliseconds when writing GPX files. For example, 12:00:00.123456 was written as 12:00:00.123.

  • Decimal values (e.g. coordinates very close to the equator or prime meridian) are now always written in fixed-point notation. Values such as lat="1E-8" were not valid xsd:decimal values. validate() now also reports decimals in exponent notation as errors.

  • Parsing now ignores surrounding whitespace in non-string values (e.g. <time> 2024-01-01T00:00:00Z </time>), just like validate(). Such documents previously passed validation but then failed to parse. Whitespace-only values are now treated as missing.

  • crop(), trim(), filter_points(), reduce_precision(), simplify(), smooth() and gpx edit now recompute the metadata bounds (if present) from the remaining points. The bounds are removed if no points remain. The original, stale bounds were previously kept.

  • reverse() with routes=False or tracks=False no longer returns a GPX that shares its route or track list with the input. strip_metadata() on a GPX without metadata now returns a new instance instead of the input itself.

  • in checks on Extensions (e.g. "hr" in extensions) now return True for matching elements without text content.

  • Copyright years with a timezone (e.g. <year>2004Z</year>) are now parsed, and written back with their timezone. They previously passed validate() but then failed to parse. Years before 1000 are now written with four digits (e.g. 0999), as xsd:gYear requires.

  • gpx edit --start/--end now accept any ISO 8601 datetime, including fractional seconds (e.g. 2024-01-01T10:00:00.5Z). Datetimes without a timezone are still interpreted as UTC.

2026.3.0 - 2026-05-17

This third release in the year 2026 adds a new GeoGPXModel base class for GPX models that carry geometric data.

Added

  • New GeoGPXModel base class for GPX models that carry geometric data (Waypoint, Route, TrackSegment, Track, Bounds, GPX). Enables isinstance() checks and type-hinted handling of geometric models, and enforces that subclasses implement __geo_interface__.

2026.2.0 - 2026-04-27

This second major release in the year 2026 adds support for GPX extensions, and includes a range of bug fixes and robustness improvements.

Added

  • GPX Extensions support: Added Extensions class for handling GPX extension elements from any XML namespace (e.g., Garmin’s TrackPointExtension). Extensions are now parsed, preserved, and serialized during round-trip processing, enabling lossless handling of vendor-specific data like heart rate, cadence, temperature, etc.

  • New extensions field on all models that support extensions per the GPX 1.1 spec: GPX, Metadata, Waypoint, Track, TrackSegment, and Route.

Changed

  • Track now inherits from PointsMixin and reuses its bounds and elevation aggregations (bounds, _points_with_ele, _eles, avg_elevation, min_elevation, max_elevation, diff_elevation, avg_speed, avg_moving_speed), eliminating duplication between Track and the segment/route mixin. Per-segment semantics are preserved for distance, duration, speed extremes, ascent/descent and the elevation profile by overriding only those properties.

  • int and bool fields in __geo_interface__ properties (e.g. Waypoint.sat, Track.number, Route.number) now keep their JSON-native types.

Removed

  • Removed obsolete remove_encoding_from_string function.

Fixed

  • EWKB format parsing now correctly handles Z coordinates by checking the EWKB flag before the ISO WKB flag.

  • gpx edit no longer drops custom XML namespace prefixes (e.g. gpxtpx, gpxx) from the source file. All edit transformations (crop, trim, reverse, precision, strip metadata) now propagate nsmap so round-trips preserve the original prefixes.

  • gpx edit --min-lat 0 (and the other crop bounds) is no longer silently ignored.

  • Track.elevation_profile no longer skips the first elevation-bearing point of segments after the first, and no longer raises IndexError for tracks with no segments or no points with elevation. PointsMixin.elevation_profile (used by Route and TrackSegment) similarly returns [] for empty inputs instead of crashing.

  • Waypoint.speed_to no longer raises ZeroDivisionError when two waypoints share a timestamp (or either lacks one); it returns 0.0 instead. This also fixes downstream crashes in PointsMixin._speeds, max_speed, min_speed, moving_duration, and speed_profile for tracks containing consecutive points with identical timestamps.

  • Waypoint.slope_to similarly returns Decimal(0) instead of raising ZeroDivisionError when two waypoints share coordinates.

2026.1.0 - 2026-01-07

This first major release in the year 2026 adds two new features to the package: file format conversion (i.e. from GPX to GeoJSON, KML, WKT and/or WKB and vice versa) and a command-line interface (CLI).

Added

  • New cli module with command-line interface (CLI) for common GPX operations:

    • gpx validate: Validate a GPX file

    • gpx info: Show information and statistics about a GPX file

    • gpx edit: Edit a GPX file with various transformations:

      • Crop to a geographic bounding box (--min-lat, --min-lon, --max-lat, --max-lon)

      • Trim to a date/time range (--start, --end)

      • Reverse routes and/or tracks (--reverse, --reverse-routes, --reverse-tracks)

      • Strip metadata fields (--strip-name, --strip-desc, --strip-author, --strip-copyright, --strip-time, --strip-keywords, --strip-links, --strip-all-metadata)

      • Reduce coordinate precision (--precision, --elevation-precision)

    • gpx merge: Merge multiple GPX files into one

    • gpx convert: Convert between GPX, GeoJSON, and KML formats

  • New CLI reference documentation page with auto-generated help output using cog

  • New io module with read_*() functions for reading file formats:

    • read_gpx(): Read GPX files

    • read_geojson(): Read GeoJSON files

    • read_kml(): Read KML files

  • New convert module with from_*() functions for converting from data formats:

    • from_string(): Convert from a GPX string

    • from_geo_interface(): Convert from objects that implement the __geo_interface__ protocol (e.g., Shapely)

    • from_wkb(): Convert from Well-Known Binary bytes

    • from_wkt(): Convert from Well-Known Text strings

  • New write_*() methods on the GPX class for writing to file formats:

    • write_gpx(): Write to GPX file (renamed from to_file())

    • write_geojson(): Write to GeoJSON file

    • write_kml(): Write to KML file (Google Earth format)

  • New to_*() methods on the GPX class for converting to data formats:

    • to_wkt(): Convert to Well-Known Text (OGC standard)

    • to_wkb(): Convert to Well-Known Binary (OGC standard)

Changed

  • Renamed GPX.to_file() to GPX.write_gpx() for consistency with other write methods.

  • Replaced built-in tempfile module usage with pytest’s tmp_path and tmp_path_factory fixtures in the test suite for better test isolation and automatic cleanup.

Removed

  • All aliases and proxies.

  • GPX.from_file() method in favor of io.read_gpx().

  • GPX.from_string() method in favor of convert.from_string().

2025.1.0 - 2025-11-29

This is a major release with a lot of breaking changes, primarily due to a complete rewrite of the architecture, and a general modernization of the package. Besides the change in architecture, this release adds comprehensive unit tests, enforces strict linting rules, and drops support folder older versions of Python. Finally, this release implements the __geo_interface__ protocol for all GPX elements that contain geopgraphic information, thus adding support for converting to GeoJSON.

Migration notes for users upgrading from 0.2.x:

  1. Ensure you are using Python 3.11 or higher

  2. Update constructor calls to use keyword arguments for optional parameters

  3. Update field names to use GPX-standard names (e.g., trkseg instead of trksegs)

  4. Remove validate=True from from_file() and from_string() calls

  5. Update error handling to catch standard Python exceptions instead of gpx.errors.ParseError

Example:

# Before (0.2.x)
from gpx import GPX, Waypoint

gpx = GPX.from_file("path/to/track.gpx", validate=True)
waypoint = Waypoint()
waypoint.lat = Decimal("52.0")
waypoint.lon = Decimal("4.0")
waypoint.name = "Amsterdam"
gpx.waypoints.append(waypoint)

# After (2025.1.0)
from gpx import GPX, Waypoint
from decimal import Decimal

gpx = GPX.from_file("path/to/track.gpx")
waypoint = Waypoint(Decimal("52.0"), Decimal("4.0"), name="Amsterdam")
gpx.wpt.append(waypoint)

Added

  • CLAUDE.md for easier interoperability with Claude Code.

  • Comprehensive unit tests.

  • Smoke test.

  • Testing workflow via GitHub Actions.

  • Support for Python 3.12, 3.13 and 3.14.

  • More usage examples in README.md.

  • __geo_interface__ protocol for all GPX elements that contain geographic information.

Changed

  • Upgraded the pre-commit hooks.

  • Enabled ALL rules for the ruff linter by default.

  • Use the uv build backend instead of flit.

  • Updated the installation instructions to make use of uv instead of pip.

  • Switched the versioning scheme from semantic versioning to Calendar Versioning.

  • Refactored the GPX element classes into dataclass-based models.

  • Replaced lxml with the built-in ElementTree module.

  • Optional dependencies to dependency groups.

  • Renamed PyGPX to gpx (purely aesthetically, no changes in installation or usage required).

Removed

  • Support for Python 3.7, 3.8, 3.9 and 3.10.

  • The errors module.

Fixed

  • Erroneous examples in the usage examples in README.md.

0.2.1 (2023-04-09)

Fixed

  • Serialization of route and track numbers. #10

  • Mutable default values. #11

0.2.0 (2023-04-06)

With this release, PyGPX is now fully [1] compatible with the GPX 1.1 specification.

Changes

0.1.1 (2023-03-31)

This release introduces a completely overhauled codebase. For this, I used my cookiecutter-python-package Python package template. As such, this release comes with much higher code quality, documentation and automation.

The API itself has not changed (and as such, there is no minor version bump).

Changes

  • Completely refactored codebase, with:

    • Moved source code from ./gpx to ./src/gpx

    • Fully typed and documented

  • Added documentation via Read the Docs

  • Updated CI/CD via GitHub Actions

  • Added pre-commit hooks w/ CI-integration

0.1.0 (2021-06-08)

Changes

  • Initial release of PyGPX