To find the correct EPSG code, first decide whether you need display, distance, or area, then let pyproj resolve the UTM zone or national grid for your area of interest and verify the units and axis order before you reproject.

Jump to heading Why this matters

Picking a CRS by guessing — or copying one from an old script — is how layers end up in the wrong place and area statistics come out wrong. The right code depends on both the region and the task: a dashboard covering France wants EPSG:2154 for measurement but EPSG:3857 for its tile map, and a dataset over central Europe wants a different UTM zone than one over the UK. Rather than memorise every zone, you can query the code programmatically for whatever bounding box your data covers, which is far more reliable than eyeballing a zone map. This task sits directly under the CRS & coordinate systems reference, which explains the geographic-versus-projected distinction the lookup depends on.

Resolving an EPSG code from task and regionThe task and a bounding box feed a lookup. A display task resolves to EPSG:3857. A distance task runs a UTM zone query in pyproj and resolves to a local UTM code such as 32633 or a national grid such as 27700. An area task resolves to an equal-area code such as 3035, 5070, or 6933.Task + bboxwhat & wheredisplaydistanceareaWeb MercatorEPSG:3857query_utm_crspyprojLocal UTM / national grid32633 · 27700 · 25832Equal-area3035 · 5070 · 6933

Jump to heading Prerequisites

  • Python 3.10+ with pyproj>=3.5 and geopandas>=0.14
  • A rough bounding box or centre point for your data, in WGS 84 degrees
  • Familiarity with the .crs attribute on a GeoDataFrame, covered in the GeoDataFrame schema reference

Jump to heading Step-by-step solution

Jump to heading Step 1 — Decide what the code is for

The lookup branches on the task. If you only need to draw the data on a web map, you want EPSG:3857 regardless of region and can stop here. If you need distance, you want a local UTM zone or national grid. If you need area, you want an equal-area CRS. Write down the task first, because it changes which of the next steps applies.

python
task = "distance"   # one of: "display", "distance", "area"

Jump to heading Step 2 — Query the UTM zone for your area of interest

For distance work, pyproj can resolve the UTM zone directly from a bounding box. This is the reliable way to get the right zone without reading a map.

python
from pyproj.database import query_utm_crs_info
from pyproj.aoi import AreaOfInterest

# Bounding box around Berlin (lon/lat degrees)
aoi = AreaOfInterest(
    west_lon_degree=13.0, south_lat_degree=52.3,
    east_lon_degree=13.8, north_lat_degree=52.7,
)
utm_list = query_utm_crs_info(datum_name="WGS 84", area_of_interest=aoi)
epsg = int(utm_list[0].code)
print(epsg, utm_list[0].name)     # 32633 WGS 84 / UTM zone 33N

query_utm_crs_info returns the zones intersecting the bounding box, northern hemisphere first. For a compact area you take the first result.

Six degrees wide, and what happens at the seamA strip of UTM zones is drawn across a band of longitude, each six degrees wide and numbered, with the central meridian of each marked. An area of interest sitting near a central meridian is shaded in green: distortion there is at its smallest, a scale factor of about 0.9996, and any measurement is accurate to well under a metre per kilometre. A second area straddling the boundary between two zones is shaded in red. Neither zone is wrong for it, but whichever is chosen, part of the area sits beyond that zone's edge where the scale factor has grown, and a feature that crosses the seam will have its two halves measured under different projections if the frame is split. The options are drawn beneath: pick the zone containing most of the area and accept the edge error, use a national grid that covers the whole extent in one projection, or use an equal-area projection when only totals matter and shape does not.UTM ZONES ARE SIX DEGREES WIDEzone 30zone 31zone 32zone 33zone 34AOI · k≈0.9996AOI across the seamWhen the extent straddles a boundary, pick deliberatelytake the zone holding most of the area and accept that distortion grows toward the far edgeuse a national grid that covers the whole extent in one projection — this is what they exist foruse an equal-area projection when only totals matter and local shape does notWhat you must not do is split the frame by zone and measure each half separately: the two halves are then in differentcoordinate systems, and any feature crossing the seam is measured twice under different rules. ### Step 3 — Resolve and inspect a candidate code

Whether the code came from the UTM query, a national-grid choice, or an equal-area choice, load it with CRS.from_user_input (which accepts EPSG integers, strings, and PROJ) and inspect what you got before trusting it.

python
from pyproj import CRS

crs = CRS.from_user_input(epsg)
print(crs.name)                      # WGS 84 / UTM zone 33N
print(crs.axis_info[0].unit_name)    # metre
print(crs.is_projected)              # True
print(crs.area_of_use.bounds)        # (12.0, 0.0, 18.0, 84.0)

The area_of_use.bounds check is the important one: confirm your data actually falls inside that longitude/latitude box, or the projection will distort.

Jump to heading Step 4 — Verify units and axis order, then reproject

Before using the code in the dashboard, confirm the units are metres for measurement work and reproject a copy to sanity-check the coordinate ranges.

python
import geopandas as gpd

gdf = gpd.read_file("berlin_districts.geojson")   # EPSG:4326
metric = gdf.to_crs(epsg)

assert crs.axis_info[0].unit_name == "metre"
# UTM eastings sit near 500,000 at the central meridian
minx, _, maxx, _ = metric.total_bounds
assert 100_000 < minx < 900_000, "eastings out of UTM range — wrong zone?"
gdf["area_km2"] = metric.area / 1_000_000
Three wrong codes, three different kinds of wrongA forty kilometre measurement is repeated under three plausible mistakes. Choosing the neighbouring UTM zone adds tens of metres and grows with distance from that zone's central meridian — small enough to survive review, large enough to matter for engineering work. Choosing the right projection on the wrong datum adds a roughly constant offset of a couple of hundred metres in position while leaving distances almost unchanged, which is the hardest of the three to notice because every length checks out and only the alignment against another layer is off. Choosing Web Mercator at fifty-five degrees north overstates the length by about seventy-four percent, which is obvious the moment anybody compares it to a known distance. The three fail differently, so the verification differs too: check the scale factor for the first, check a known control point for the second, and check the units for the third.MEASURING 40 km UNDER THREE PLAUSIBLE MISTAKESneighbouring UTM zone+ tens of metres, growing with distance from the central meridiansurvives review; matters for engineering — catch it by checking the scale factorright projection, wrong datum≈ 200 m constant offset in position, lengths almost unchangedthe hardest to spot — every distance checks out, only the alignment is wrongWeb Mercator at 55° N+74% — 69.6 kmobvious against any known distance — catch it by checking the units of the frameThe three fail in different ways, so no single check finds all of them: verify the scale factor, verify a known controlpoint, and verify the units — in that order of increasing obviousness and decreasing cost. ## Region → recommended EPSG

For quick reference, these are sound defaults for measurement in common regions. Display always uses EPSG:3857.

RegionRecommended EPSGNameNotes
United Kingdom27700OSGB36 / British National GridNational grid, whole country
France (metropolitan)2154RGF93 / Lambert-93National grid
Germany / central EU25832ETRS89 / UTM zone 32N6°E–12°E
Eastern EU / Poland32633WGS 84 / UTM zone 33N12°E–18°E
Europe (area stats)3035ETRS89-extended / LAEA EuropeEqual-area
Contiguous US (area)5070NAD83 / Conus AlbersEqual-area
Global (area stats)6933WGS 84 / EASE-Grid 2.0Equal-area

Jump to heading Verification

Confirm the code round-trips cleanly and produces measurements in the right order of magnitude:

python
from pyproj import CRS

crs = CRS.from_epsg(epsg)
assert not crs.is_deprecated, f"EPSG:{epsg} is deprecated — migrate to its replacement"
assert crs.is_projected, "expected a projected CRS for distance/area work"
# A Berlin district area should be single- to double-digit km², never < 0.01
assert gdf["area_km2"].between(0.01, 10_000).all()

Jump to heading Edge cases and gotchas

  • UTM zone boundaries. A point at 12.0°E sits exactly on the boundary between zones 32N and 33N. query_utm_crs_info may return both; pick the zone that contains the bulk of your data, not a single edge point.
  • Cross-zone areas. If your data spans more than one 6° UTM band, do not force a single zone — distortion grows fast at the edges. Switch to a national grid or the regional equal-area code from the table above.
  • Deprecated codes. Older datasets sometimes carry superseded codes. CRS.from_epsg(code).is_deprecated flags them; migrate to the replacement the warning names so you do not inherit an old datum definition.

Jump to heading FAQ

How do I find the UTM zone for a coordinate in Python?

Use pyproj.database.query_utm_crs_info with an AreaOfInterest bounding box around your point. It returns the matching UTM CRS records, and you read the EPSG code from the first result’s code attribute. UTM zones are 6° of longitude wide, so a bounding box around a single point resolves to exactly one zone.

What EPSG code should I use if my region spans two UTM zones?

Do not force a single UTM zone across a wide region — distortion grows quickly outside a zone’s 6° band. Use a national grid such as EPSG:27700 for the UK, or a regional equal-area projection such as EPSG:3035 for Europe, that is designed to cover the whole area at acceptable distortion.

How do I know if an EPSG code is deprecated?

Build the CRS with pyproj.CRS.from_epsg and check its is_deprecated attribute. Deprecated codes still resolve for backward compatibility, but pyproj flags them and you should migrate to the current replacement code to avoid subtle datum differences.


Back to CRS & Coordinate Systems Reference

Related