Open-source project
Turbo87/utm avatar
Turbo87/utm

utm is a small converter that keeps its zone and letter conventions consistent

Bidirectional UTM-WGS84 converter for python

532 stars110 forksPythonMIT

At a glance

What is it?
utm is a Python library that converts between WGS84 latitude and longitude and UTM coordinates in both directions, with optional NumPy vectorisation and no runtime dependencies. It is old, MIT licensed and maintained, and its README is precise about where it should not be used.
Who is it for?
Adopt utm if you need UTM coordinates and want a dependency-free conversion with a documented zone convention, particularly if you already have NumPy and want to convert a large point set in one call. Do not adopt it for anything beyond UTM, since it covers no other projection and no datum shift, and treat its polar behaviour as a limit rather than an edge case.
Can I use it commercially?
Yes. MIT 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 13 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 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two functions, one tuple in and one tuple out

utm is a bidirectional UTM-WGS84 converter for Python, and it does exactly one thing in each direction.

Converting a latitude and longitude into UTM is a single call.

python
  >>> utm.from_latlon(51.2, 7.5)
  (395201.3103811303, 5673135.241182375, 32, 'U')

The syntax is `utm.from_latlon(LATITUDE, LONGITUDE)` and the return has the form `(EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER)`.

Going the other way is the mirror image.

python
  >>> utm.to_latlon(340000, 5710000, 32, 'U')
  (51.51852098408468, 6.693872395145327)

The syntax is `utm.to_latlon(EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER)` and the return is `(LATITUDE, LONGITUDE)`.

The naming is the part worth internalising, because the argument order changes between the two. In from_latlon the inputs are degrees in latitude then longitude, matching the `(latitude, longitude)` tuple described in the README. In to_latlon the inputs are metres in easting then northing. Getting that backwards produces plausible-looking numbers rather than an error, which is the kind of mistake that survives a code review.

The package is on PyPI as utm, the homepage given in the repository metadata is the PyPI project page, and the licence is MIT with the copyright line reading 2012-2017 Tobias Bieniek.

NumPy arrays work, but the zone comes from the first point

Both functions accept NumPy arrays, and the README explains the shape rules and one sharp edge.

For from_latlon with arrays, the result is arrays.

python
  >>> utm.from_latlon(np.array([51.2, 49.0]), np.array([7.5, 8.4]))
  (array([395201.31038113, 456114.59586214]),
   array([5673135.24118237, 5427629.20426126]),
   32,
   'U')

In the result, EASTING and NORTHING will have the same shape. ZONE_NUMBER and ZONE_LETTER are scalars and will be calculated for the first point of the input, and all other points will be set into the same UTM zone. Therefore it is a good idea to make sure all points are near each other.

That last sentence is the important one, and it is a genuine correctness constraint rather than a performance hint. If you feed in points from two different UTM zones, the library does not warn you and does not split the output. It computes one zone from the first point and projects everything else into it. The coordinates that come back are the ones that would be produced by that projection, and they are wrong for the other zone.

So a batch conversion is safe only when the batch spans a single zone. Points from a country-sized dataset, or a dataset spanning a zone boundary, need to be split first, and that splitting is your responsibility rather than the library's.

For to_latlon the behaviour is the same in reverse. LATITUDE and LONGITUDE have the same shape as the inputs, and ZONE_NUMBER and ZONE_LETTER are scalars.

The zone letter is optional, and there is a northern flag instead

There is a small convenience in the API that removes one argument, and it is worth knowing about because it is easy to trip over.

Since the zone letter is not strictly needed for the conversion, you may also use the northern parameter instead, which is a named parameter and can be set to either True or False. The README points at the unit tests to see how it can be used.

That is a small thing, but it changes the shape of the call. Instead of passing a zone letter string you pass a boolean, and the library derives the hemisphere from it. For a codebase that consistently deals with one hemisphere, that removes a string that was always the same value.

For a codebase that spans hemispheres, it is a trap, because the flag is per call rather than per dataset, and nothing in the API lets you pass a latitude and longitude and have the library decide for itself.

The zone number is not optional, since it selects the central meridian of the projection. The letter is the hemisphere indicator in the standard band notation, and it is the part this library treats as redundant.

For background, the README points at the Wikipedia page explaining the UTM coordinate system, which is the right reference if you need to know why the zone number matters rather than just how to pass it.

The speed argument, and what the comparison actually measured

The README has a Speed section that compares the library to pyproj, and the way the comparison was run is more informative than the numbers.

The library has been compared to the more generic pyproj library by running the unit test suite through pyproj instead of utm. The results were four to four and a half seconds with pyproj without a projection cache, zero point nine to one second with pyproj with a projection cache, and zero point four to zero point five seconds with utm.

So the comparison is not a synthetic benchmark of raw conversion throughput, it is the same test suite executed two ways. That is a fairer comparison in one sense, since it measures the same work, and a weaker one in another, since a test suite is not representative of your workload.

The NumPy figure is the more striking one and is measured separately, on a different computer than the previous test. Using `utm.from_latlon(x, y)` to convert one million points, one million calls with floats took 1,000,000 times 90 microseconds, so 90 seconds, while a single call with NumPy arrays of one million points took 0.26 seconds.

That is a three hundred fold difference from batching alone, and it is the argument for the optional NumPy dependency. The core package has no dependencies at all, and NumPy is an optional extra, so you can take the fast path only if you already use NumPy.

The packaging supports this carefully. The optional-dependencies table has numpy1 and numpy2 groups, each pinning a NumPy version by Python version, with a comment explaining that within each major NumPy version the latest minor version each Python version supports is used. numpy1 pins 1.26.4 for Python 3.9 to 3.12, and numpy2 has entries for 2.5.1, 2.4.6 and 2.2.6 with their own Python ranges.

Setting up for development, three ways

The development section gives three routes, and the choice is about how much you want the tooling to manage for you.

Using uv is the easiest: run `uv sync`.

bash
uv sync

Using pipx, the command installs the package in editable mode along with the development dependency group.

bash
pipx install -e . --pip-args="--group dev"

And using pip requires manually setting up the virtual environment: create and activate a new virtualenv, then run `python -m pip install -e . --group dev`.

The `--group dev` part is worth understanding rather than copying. The pyproject.toml separates the runtime requirements from the development ones using PEP 735 dependency groups, with a test group holding pytest and pytest-cov, both pinned by Python version. The project itself declares dependencies = [] , so nothing is installed at runtime unless you ask for it.

That is why the NumPy story works the way it does. There is no core dependency, and the optional-dependencies table carries numpy1 and numpy2, so pip and uv resolve a NumPy that matches your Python version.

Tests are run with pytest after the environment is prepared. The repository has a test/ directory, and the licence file is a LICENSE at the top level alongside a MANIFEST.in and a CHANGELOG.rst. There is also a renovate.json at the top level, which means the dependency pins in the packaging are being kept current automatically.

Where utm is the wrong tool

Three cases, and the first is a hard boundary on the projection.

This library does UTM and nothing else. There is no other projection, no geographic to geocentric conversion, no datum shift beyond what UTM itself implies, and no support for the polar regions. UTM is not defined at the poles, and the library does not document any special handling there, so a point near the pole produces a number without a meaningful interpretation. The README is precise about the zone conventions but says nothing about the polar limits.

The second is the batch zone hazard, which is worth restating because it is a silent failure. If you convert a set of points that spans more than one UTM zone in a single call, the library uses the first point's zone for all of them and does not tell you. For a mapping or logistics workflow that silently relocates points by hundreds of kilometres, the fix is to group by zone before calling, and the README's advice that it is a good idea to make sure all points are near each other is a hint rather than a warning.

The third is if you need a general projection library. pyproj exists, it covers every CRS you are likely to need, and the README's own comparison puts it within a factor of two of this library once its projection cache is enabled. utm wins on the fast path, on being dependency free, and on a small API. If your work involves more than one coordinate system, the general tool is the right one and this is a specialised shortcut.

One more practical detail. The package metadata declares requires-python of 3.10 or newer while the classifiers list Python 3.9 as well, down to 3.14 at the top end. The classifiers are broader than the enforced floor, and pip will follow the requires-python field.

Editorial conclusion

Adopt utm if you need UTM coordinates and want a dependency-free conversion with a documented zone convention, particularly if you already have NumPy and want to convert a large point set in one call. Do not adopt it for anything beyond UTM, since it covers no other projection and no datum shift, and treat its polar behaviour as a limit rather than an edge case. Verify first which hemisphere convention your data uses, because the zone letter is optional and the northern parameter is the documented alternative, and check the unit tests before you rely on the boundary behaviour near zone edges.

Frequently asked questions

How do I convert latitude and longitude to UTM with the utm library?

Call utm.from_latlon(LATITUDE, LONGITUDE), which returns a tuple of (EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER). For example utm.from_latlon(51.2, 7.5) returns (395201.3103811303, 5673135.241182375, 32, 'U').

How do I convert UTM coordinates back to latitude and longitude?

Call utm.to_latlon(EASTING, NORTHING, ZONE_NUMBER, ZONE_LETTER), which returns (LATITUDE, LONGITUDE). The zone letter is optional since the zone letter is not strictly needed for the conversion, so you may use the northern parameter instead, set to either True or False.

Does the utm library support NumPy arrays?

Yes. Both functions accept NumPy arrays and the result arrays have the same shape as the input. ZONE_NUMBER and ZONE_LETTER are scalars calculated for the first point of the input, and all other points are set into the same UTM zone, so the README advises making sure all points are near each other.

Does the utm library have any runtime dependencies?

The core package declares no dependencies at all. NumPy is available as optional extras, with numpy1 and numpy2 groups that pin a NumPy version appropriate to each Python version, and the README reports that converting one million points in a single array call took 0.26 seconds against 90 seconds for one million separate calls.

How do I set up a development environment for utm?

The README gives three options: run uv sync, which is described as the easiest, run pipx install -e . --pip-args="--group dev", or create and activate a virtualenv manually and run python -m pip install -e . --group dev. Tests are then run with pytest.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. Turbo87/utm 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/turbo87-utm.svg)](https://hysenlabs.com/projects/turbo87-utm)

Was this useful?