Library / SDK
tkrajina/gpxpy avatar
tkrajina/gpxpy

gpxpy reads GPS tracks and hands you an object model, not an XML tree

gpx-py is a python GPX parser. GPX (GPS eXchange Format) is an XML based file format for GPS tracks.

1,106 stars235 forksPythonApache-2.0

At a glance

What is it?
gpxpy is a Python library for parsing and manipulating GPX files, the XML based format for GPS tracks, with an object model covering tracks, segments, points, waypoints and routes, plus statistics that filter out common GPS errors. It reads and writes GPX 1.0 and 1.1, preserves extensions, and is the library behind the author's online track editor.
Who is it for?
Adopt gpxpy when you need to read, filter, edit and write GPS tracks in Python, especially when the data comes from consumer devices that produce files with the usual GPS noise. Do not treat it as a strict schema validator, because the object model is explicitly not a hundred percent equivalent to the GPX XML schema, and a file it writes is valid XML but may not be strictly valid GPX.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 27 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

A parser with a real object model behind it

gpxpy is a simple Python library for parsing and manipulating GPX files, where GPX is an XML based format for GPS tracks.

The difference between a parser and a model shows up in the usage example, because the API is an object graph rather than a DOM. You call gpxpy.parse on a file and get back an object with tracks, each of which has segments, each of which has points, and alongside the tracks there are waypoints and routes.

python
import gpxpy
import gpxpy.gpx

gpx_file = open('test_files/cerknicko-jezero.gpx', 'r')

gpx = gpxpy.parse(gpx_file)

for track in gpx.tracks:
    for segment in track.segments:
        for point in segment.points:
            print(f'Point at ({point.latitude},{point.longitude}) -> {point.elevation}')

The point objects are attributes rather than dictionaries, and the same model is used in the other direction. You can create a GPX object, append a track, append a segment to that track, append points to the segment, and serialise the whole thing with to_xml(). The read and write paths go through the same classes, which is the property that makes the library useful for editing rather than only for reading.

The author shows it in action on an online GPS track editor and organiser, and there is also a Golang port of gpxpy called gpxgo for the same format. The implementation is Python, the licence is Apache-2.0, and the last push was on 2026-09-26.

The object model is deliberately not the schema

The GPX version section is the most important thing in the readme for anyone who writes files back out, and it is unusually candid about the compromises.

gpx.py can parse and generate GPX 1.0 and 1.1 files. The generated file will always be a valid XML document, but it may not be, strictly speaking, a valid GPX document. The example given is setting gpx.email to a value that does not match the expected pattern, and most applications will ignore such errors, but the readme says to be aware of it.

The reason is stated next. The gpxpy object model is not 100 percent equivalent with the underlying GPX XML file schema, because the library object model works with both GPX 1.0 and 1.1.

The concrete example is the speed attribute. GPX 1.0 specified a speed attribute for every track point, and 1.1 removed it. Parsing a 1.0 file and serialising it back works fine, but if you have a 1.1 object, changes in the speed attribute are lost after to_xml(). There are two ways out, and the readme gives both: force the version with gpx.to_xml(version="1.0"), or use extensions to save the speed in a 1.1 document.

That is a real constraint and it is the kind that bites when a downstream tool rejects your file. A library that pretended the two versions were the same would lose data silently, and this one tells you where it does.

Extensions survive a round trip

GPX extensions are part of GPX 1.1, and gpx.py preserves them, storing them as ElementTree DOM objects.

So the extension data is not parsed into a typed model, it is kept as the XML that was there. That is a deliberate trade in both directions. You get everything the device recorded, including vendor specific fields the library has never heard of, and you do not get a nice attribute access for them, because there is no schema to generate an attribute from.

And extensions are ignored when serialising a GPX object into a 1.0 file, since 1.0 has no extension mechanism. So the version of the document decides whether that data survives the write, which is another reason to check what version you are actually working with before you start a round trip.

The examples directory has two files that show the extension path in use, extension_write_example.py and waypoints_example.py, and the readme's main usage example is followed by a note that there are many more utility methods and functions for manipulating and adding tracks, segments, points, waypoints and routes.

The version is detected, and can be forced

The XML parsing section is short and answers a question that has bitten everyone who has parsed a GPX file from a device they did not choose.

The GPX version is automatically determined when parsing by reading the version attribute in the gpx node. If that attribute is not present then the version is assumed to be 1.0. A specific version can be forced by setting the version parameter in the parse function, and the possible values for it are 1.0, 1.1 and None.

The default of 1.0 when the attribute is missing is the right default, because most files written by older devices omit it and are 1.0 documents, and because the 1.1 model is the more permissive one. The None value is for a file where you want the library to read what it can and not assume either.

That is also the setting to reach for when a file parses strangely. A 1.1 file that omits the version attribute will be read as 1.0, and the difference in behaviour is exactly the speed attribute case described above, so forcing the version is the first thing to try when points come out of a file that should have had more.

The statistics throw out the GPS errors

The max speed section is short, and it explains what makes the calculated statistics different from the raw file.

Gpxpy is a GPX parser, and by using it you have access to all the data from the original GPX file. The additional methods to calculate stats have some additional heuristics to remove common GPS errors. For example, to calculate max_speed it removes the top 5 percent of speeds and points with nonstandard distance, which are usually GPS errors.

That is a meaningful difference between the number in the file and the number the library reports, and it is worth knowing which one you are looking at. A track recorded in a city has points where the receiver jumped across town, and those show up as enormous speeds.

The raw figure is available when you need it.

python
moving_data = gpx.get_moving_data(raw=True)

So the default is a cleaned statistic that is useful for comparing tracks, and raw=True gives you the unfiltered data when the cleaning is hiding something you need to see. For a library whose output feeds dashboards and reports, that distinction belongs in your documentation as much as in your code.

Contribution goes to the dev branch, and mypy is not optional

The pull requests section is specific about the workflow, and the two branch names are the first thing to learn.

The master branch contains the code of the latest release, and the dev branch is where code for the next release should go. Send your pull request against dev, not master.

Before sending a pull request, check that all the tests are OK, and run all the static typing checks and unit tests with a single make target.

bash
$ make mypy-and-tests

A single test can be run with the standard library test runner.

bash
$ python3 -m unittest test.GPXTests.test_haversine_and_nonhaversine

And the rule underneath both: gpxpy runs only with Python 3.10 and newer. The code must have type hints and must pass all the mypy checks.

The typing requirement is the part that shapes the library. A library that is fully typed, with a py.typed marker in the package data, is a library you can use from a typed codebase without casts, and the setup file ships py.typed in package_data for exactly that reason. The classifiers list Python 3.10 through 3.15, which is a forward looking range for a library that has been running since 2011.

The repository, the tools and the schemas

The top level listing shows a library with the supporting material a parser like this needs.

There is the gpxpy/ package, an examples/ directory, a test_files/ directory with the sample files the tests parse, and an xsd/ directory, which is the XML schema for the format and the reference against which the output can be checked. There is test.py as the test entry point, a makefile for the developer targets, and a gpxinfo file, which is registered as a script in the setup so it is installed onto the path as a command.

Then the project files: setup.py with the standard Apache header, a MANIFEST.in, a NOTICE.txt, a LICENSE.txt, a CHANGELOG.md, a TODO.txt, a CONTRIBUTING.md, an AGENTS.md, and the .github/ directory with the test workflow that the badge points at.

The command line tools for GPX files are a separate project, gpx-cmd-tools, which is installed from PyPI.

bash
pip install gpx-cmd-tools

Keeping the command line tools out of the library package is the right call, because the library has no dependencies and adding a tools package with its own would change that for everyone who only wants to parse a file.

The setup file is otherwise minimal, with a single package, the py.typed marker, the python_requires of 3.10 or newer, and the version taken from the package itself rather than being written twice.

Editorial conclusion

Adopt gpxpy when you need to read, filter, edit and write GPS tracks in Python, especially when the data comes from consumer devices that produce files with the usual GPS noise. Do not treat it as a strict schema validator, because the object model is explicitly not a hundred percent equivalent to the GPX XML schema, and a file it writes is valid XML but may not be strictly valid GPX. Verify first which GPX version your files use, since the version is inferred from the file and the object model silently smooths over the differences between 1.0 and 1.1.

Frequently asked questions

What is gpxpy?

It is a Python library for parsing and manipulating GPX files, the XML based format for GPS tracks. It provides an object model of tracks, segments, points, waypoints and routes, and can serialise that model back to XML.

Does gpxpy write valid GPX files?

The generated file will always be a valid XML document, but it may not be strictly a valid GPX document, because the object model is not 100 percent equivalent to the GPX XML schema. You can force the version with gpx.to_xml(version="1.0").

How does gpxpy decide which GPX version a file uses?

The version is read from the version attribute in the gpx node, and if that attribute is missing the version is assumed to be 1.0. You can force a version with the version parameter of the parse function, whose possible values are 1.0, 1.1 and None.

Are GPX extensions preserved by gpxpy?

Yes. Extensions are stored as ElementTree DOM objects, so vendor specific data survives a round trip, although extensions are ignored when serialising into a GPX 1.0 file since that version has no extension mechanism.

Why does gpxpy report a different max speed from the file?

The calculated statistics apply heuristics to remove common GPS errors, and for max_speed it drops the top 5 percent of speeds and points with nonstandard distance, which are usually GPS errors. You can get the unfiltered value with gpx.get_moving_data(raw=True).

How do I contribute to gpxpy?

Send the pull request against the dev branch rather than master, and run make mypy-and-tests first. The library runs only on Python 3.10 or newer, and the code must have type hints and pass all the mypy checks.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. tkrajina/gpxpy on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tkrajina-gpxpy.svg)](https://hysenlabs.com/projects/tkrajina-gpxpy)

Was this useful?