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
operationsmodule that exposes the editing and merging operations behind thegpx editandgpx mergeCLI commands as a public, reusable API:crop(): Crop a GPX to a geographic bounding boxtrim(): Trim a GPX to a date/time rangereverse(): Reverse the routes and/or tracks of a GPXstrip_metadata(): Strip metadata (fields) from a GPXreduce_precision(): Reduce the precision of the coordinates and/or elevations of a GPXfilter_points(): Filter the points of a GPX with an arbitrary predicatemerge(): Merge multiple GPX instances into onesplit(): Split track segments at time and/or distance gapssimplify(): Simplify the tracks and routes of a GPX with the Ramer-Douglas-Peucker algorithmsmooth(): Smooth the coordinates and/or elevations of the tracks and routes of a GPX with a moving averageshift_time(): Shift all point timestamps of a GPX by a time deltastrip_extensions(): Strip all extensions from a GPXAll operations are pure (they return a new
GPXinstance and never mutate the input) and are importable directly from the top-level package (e.g.from gpx import crop, merge).
New
gpx editCLI options exposing the new operations:--split-time-gap SECONDSand--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
iomodule, exposing the operation behind thegpx convertCLI command as a public, reusable API:convert_file(): Convert a file between the GPX, GeoJSON and KML file formatsdetect_format(): Detect the file format from a file path’s extensionBoth are importable directly from the top-level package (e.g.
from gpx import convert_file).
New
validationmodule 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 aGPXinstance and return aValidationResult.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, withis_valid,errorsandwarningsproperties.ValidationIssue: A single issue with aseverity,message,path(e.g.gpx > trk[0] > trkseg[2] > trkpt[14]) and sourceline(when available).Severity: Enum ofERRORandWARNING.InvalidGPXError: Raised by strict parsing; carries the fullValidationResult.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, copyrightyear,time).All names are importable directly from the top-level package (e.g.
from gpx import validate).
New
strictkeyword argument onread_gpx()andfrom_string(). Whenstrict=True, the input is validated against the GPX 1.1 schema first and anInvalidGPXErroris raised if any errors are found. The default (strict=False) keeps the existing lenient behavior.New
Yeartype (anintsubclass) forxsd:gYearvalues such as2004,2004Zor2004+02:00. The optional timezone is kept inYear.timezone, andstr()returns validxsd:gYeartext. It’s importable from the top-level package (from gpx import Year).New
gpx validateCLI options:--strict: Treat warnings as failures (non-zero exit code).--json: Output a machine-readable validation report.
New
--strictoption on thegpx info,gpx edit,gpx mergeandgpx convertCLI 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. Forgpx convert, validation only applies to GPX input. The default keeps the existing lenient behavior.
Changed¶
The CLI (
gpx edit,gpx mergeandgpx convert) now uses the newoperationsmodule andioconversion functions internally (behavior is unchanged).The
gpx validateCLI 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.yearis now typed asYear | Noneinstead ofint | None.Yearis anintsubclass, so existing code keeps working at runtime, but type checkers now expectYear(2004)instead of2004.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()andgpx convertto GeoJSON no longer crash on routes and tracks without any points. Their geometries are now written without abbox.Track.max_speedandTrack.min_speedno longer raise when a track segment has fewer than two points. Such segments are now skipped.NaNandInfinityvalues are now rejected with aValueErrorbyLatitude,LongitudeandDegrees(instead of an unhelpfuldecimal.InvalidOperation).validate()now reports them as errors instead of crashing.GeoJSON output (
__geo_interface__,write_geojson()andgpx 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 nopropertiesmember, which strict GeoJSON readers reject.from_wkb()now correctly reads geometries with M values (ISOM/ZMtypes and EWKB M flag) and EWKB geometries with an embedded SRID (e.g. PostGISST_AsEWKBoutput). 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()andgpx convertnow read KML files in the legacy Google Earth namespaces (http://earth.google.com/kml/2.0,2.1and2.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 aValueError.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 aUnicodeDecodeErroron other encodings.from_string()andvalidate_text()now also accept encodedbytes.from_geo_interface(),read_geojson(),from_wkt()andfrom_wkb()now raise aValueErrorfor geometries without a GPX equivalent (e.g.Polygon), including ones nested in aFeatureCollectionorGeometryCollection. They were previously dropped without any error, so e.g.from_wkt("POLYGON (...)")returned an empty GPX.from_wkb(),from_wkt()andfrom_geo_interface()now raise aValueError, as documented, for truncated WKB data and coordinates with fewer than two values. They previously raisedstruct.errororIndexError. 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.123456was written as12: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 validxsd:decimalvalues.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 likevalidate(). 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()andgpx editnow 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()withroutes=Falseortracks=Falseno 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.inchecks onExtensions(e.g."hr" in extensions) now returnTruefor 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 passedvalidate()but then failed to parse. Years before 1000 are now written with four digits (e.g.0999), asxsd:gYearrequires.gpx edit --start/--endnow 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
GeoGPXModelbase class for GPX models that carry geometric data (Waypoint,Route,TrackSegment,Track,Bounds,GPX). Enablesisinstance()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
Extensionsclass for handling GPX extension elements from any XML namespace (e.g., Garmin’sTrackPointExtension). 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
extensionsfield on all models that support extensions per the GPX 1.1 spec:GPX,Metadata,Waypoint,Track,TrackSegment, andRoute.
Changed¶
Tracknow inherits fromPointsMixinand 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 betweenTrackand 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.intandboolfields in__geo_interface__properties (e.g.Waypoint.sat,Track.number,Route.number) now keep their JSON-native types.
Removed¶
Removed obsolete
remove_encoding_from_stringfunction.
Fixed¶
EWKB format parsing now correctly handles Z coordinates by checking the EWKB flag before the ISO WKB flag.
gpx editno longer drops custom XML namespace prefixes (e.g.gpxtpx,gpxx) from the source file. All edit transformations (crop, trim, reverse, precision, strip metadata) now propagatensmapso round-trips preserve the original prefixes.gpx edit --min-lat 0(and the other crop bounds) is no longer silently ignored.Track.elevation_profileno longer skips the first elevation-bearing point of segments after the first, and no longer raisesIndexErrorfor tracks with no segments or no points with elevation.PointsMixin.elevation_profile(used byRouteandTrackSegment) similarly returns[]for empty inputs instead of crashing.Waypoint.speed_tono longer raisesZeroDivisionErrorwhen two waypoints share a timestamp (or either lacks one); it returns0.0instead. This also fixes downstream crashes inPointsMixin._speeds,max_speed,min_speed,moving_duration, andspeed_profilefor tracks containing consecutive points with identical timestamps.Waypoint.slope_tosimilarly returnsDecimal(0)instead of raisingZeroDivisionErrorwhen 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
climodule with command-line interface (CLI) for common GPX operations:gpx validate: Validate a GPX filegpx info: Show information and statistics about a GPX filegpx 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 onegpx convert: Convert between GPX, GeoJSON, and KML formats
New CLI reference documentation page with auto-generated help output using
cogNew
iomodule withread_*()functions for reading file formats:read_gpx(): Read GPX filesread_geojson(): Read GeoJSON filesread_kml(): Read KML files
New
convertmodule withfrom_*()functions for converting from data formats:from_string(): Convert from a GPX stringfrom_geo_interface(): Convert from objects that implement the__geo_interface__protocol (e.g., Shapely)from_wkb(): Convert from Well-Known Binary bytesfrom_wkt(): Convert from Well-Known Text strings
New
write_*()methods on theGPXclass for writing to file formats:write_gpx(): Write to GPX file (renamed fromto_file())write_geojson(): Write to GeoJSON filewrite_kml(): Write to KML file (Google Earth format)
New
to_*()methods on theGPXclass 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()toGPX.write_gpx()for consistency with other write methods.Replaced built-in
tempfilemodule usage with pytest’stmp_pathandtmp_path_factoryfixtures in the test suite for better test isolation and automatic cleanup.
Removed¶
All aliases and proxies.
GPX.from_file()method in favor ofio.read_gpx().GPX.from_string()method in favor ofconvert.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:
Ensure you are using Python 3.11 or higher
Update constructor calls to use keyword arguments for optional parameters
Update field names to use GPX-standard names (e.g.,
trkseginstead oftrksegs)Remove
validate=Truefromfrom_file()andfrom_string()callsUpdate 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.mdfor 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-commithooks.Enabled ALL rules for the
rufflinter by default.Use the
uvbuild backend instead offlit.Updated the installation instructions to make use of
uvinstead ofpip.Switched the versioning scheme from semantic versioning to Calendar Versioning.
Refactored the GPX element classes into
dataclass-based models.Replaced
lxmlwith the built-inElementTreemodule.Optional dependencies to dependency groups.
Renamed
PyGPXtogpx(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¶
Added the following new modules and classes:
gpx.element.Elementgpx.mixins.AttributesMutableMappingMixingpx.mixins.PointsSequenceMixingpx.mixins.PointsMutableSequenceMixin
All element classes now inherit from the new
gpx.element.Elementbase class.All element classes now make use of the new element / type classes (e.g.
gpx.types.Latitude), according to the GPX 1.1 specification.Added the
gpx.errors.ParseErrorexception for when attempting to parse an XML element that does not exist.All metadata attributes have been moved to the new
gpx.metadata.Metadataclass, but are still accessible via thegpx.gpx.GPXclass via aliases for backwards compatibility and convenience.Changed all
durationattributes todatetime.timedeltaobjects (instead offloats).
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
./gpxto./src/gpxFully 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