Type System

The pygeohash library provides a comprehensive type system to help users write type-safe code and get better IDE support. This includes both basic types for working with geohashes and specialized types for NumPy and Pandas integration.

Core Types

These are the fundamental types used throughout the library:

LatLong

A named tuple representing a latitude/longitude coordinate pair:

from pygeohash import LatLong

location: LatLong = LatLong(latitude=37.7749, longitude=-122.4194)
print(f"Latitude: {location.latitude}, Longitude: {location.longitude}")
ExactLatLong

A named tuple that includes error margins for latitude and longitude:

from pygeohash import ExactLatLong, decode_exactly

exact_location: ExactLatLong = decode_exactly("9q9hwg")
print(f"Lat: {exact_location.latitude} ± {exact_location.latitude_error}")
print(f"Lon: {exact_location.longitude} ± {exact_location.longitude_error}")
BoundingBox

A named tuple representing a geographical bounding box:

from pygeohash import BoundingBox, get_bounding_box

bbox: BoundingBox = get_bounding_box("9q9hwg")
print(f"Min Lat: {bbox.min_lat}, Max Lat: {bbox.max_lat}")
print(f"Min Lon: {bbox.min_lon}, Max Lon: {bbox.max_lon}")

Validation Types

The library provides special types and validation functions for geohashes and coordinates. These help catch errors early and make code more maintainable.

Validation Functions

The library provides three pairs of validation functions:

  1. Check functions that return boolean: - is_valid_geohash(value: str) -> bool - is_valid_latitude(value: float) -> bool - is_valid_longitude(value: float) -> bool

  2. Assertion functions that return validated types: - assert_valid_geohash(value: str) -> Geohash - assert_valid_latitude(value: float) -> Latitude - assert_valid_longitude(value: float) -> Longitude

Basic Usage

Here’s how to use the validation functions:

from pygeohash import (
    Geohash, Latitude, Longitude,
    assert_valid_geohash, assert_valid_latitude, assert_valid_longitude,
    is_valid_geohash, is_valid_latitude, is_valid_longitude,
)

# Using check functions (returns bool)
if is_valid_geohash("9q9hwg"):
    print("Valid geohash!")

if is_valid_latitude(37.7749) and is_valid_longitude(-122.4194):
    print("Valid coordinates!")

# Using assertion functions (returns validated type)
try:
    geohash: Geohash = assert_valid_geohash("9q9hwg")
    lat: Latitude = assert_valid_latitude(37.7749)
    lon: Longitude = assert_valid_longitude(-122.4194)
except ValueError as e:
    print(f"Validation failed: {e}")

Validation Rules

The validation functions enforce the following rules:

  1. Geohash validation: - Must contain only base32 characters (0-9, b-h, j-k, m-n, p-z) - Must be between 1 and 12 characters long - Cannot be empty or None

  2. Latitude validation: - Must be between -90 and 90 degrees inclusive - Must be a valid float number

  3. Longitude validation: - Must be between -180 and 180 degrees inclusive - Must be a valid float number

Best Practices

Here are some recommended patterns for using the validation types:

  1. Validate early:

from pygeohash import assert_valid_geohash, assert_valid_latitude, assert_valid_longitude

def process_location(geohash: str, lat: float, lon: float) -> None:
    # Validate all inputs immediately at function start
    validated_geohash = assert_valid_geohash(geohash)
    validated_lat = assert_valid_latitude(lat)
    validated_lon = assert_valid_longitude(lon)

    # Rest of the function can assume valid data
    ...
  1. Use type hints with validated types:

from pygeohash import Geohash, Latitude, Longitude

def calculate_distance(
    geohash1: Geohash,
    lat: Latitude,
    lon: Longitude,
) -> float:
    # Function can assume inputs are already validated
    ...
  1. Handle validation errors appropriately:

from pygeohash import assert_valid_geohash, is_valid_latitude, is_valid_longitude

def safe_process_location(geohash: str, lat: float, lon: float) -> None:
    # Check without raising for coordinates
    if not is_valid_latitude(lat) or not is_valid_longitude(lon):
        print(f"Warning: Invalid coordinates ({lat}, {lon})")
        return

    try:
        # Assert for geohash (will raise if invalid)
        validated_geohash = assert_valid_geohash(geohash)
    except ValueError as e:
        print(f"Error: {e}")
        return

    # Process validated data
    ...
  1. Use with NumPy and Pandas:

import numpy as np
import pandas as pd
from pygeohash import (
    assert_valid_geohash,
    assert_valid_latitude,
    assert_valid_longitude,
)

# Validate NumPy arrays
def validate_coordinate_arrays(
    lats: np.ndarray,
    lons: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
    # Vectorized validation
    valid_lats = np.logical_and(lats >= -90, lats <= 90)
    valid_lons = np.logical_and(lons >= -180, lons <= 180)

    if not np.all(valid_lats):
        raise ValueError("Invalid latitudes found")
    if not np.all(valid_lons):
        raise ValueError("Invalid longitudes found")

    return lats, lons

# Validate Pandas Series
def validate_geohash_series(s: pd.Series) -> pd.Series:
    # Apply validation to each element
    return s.apply(assert_valid_geohash)

Common Validation Errors

Here are the common validation errors you might encounter:

  1. Invalid geohash format:

# These will raise ValueError
assert_valid_geohash("")  # Empty string
assert_valid_geohash("!")  # Invalid characters
assert_valid_geohash("9q9hwg" * 3)  # Too long (>12 chars)
  1. Invalid coordinate values:

# These will raise ValueError
assert_valid_latitude(91)  # Above 90 degrees
assert_valid_latitude(-91)  # Below -90 degrees
assert_valid_longitude(181)  # Above 180 degrees
assert_valid_longitude(-181)  # Below -180 degrees

Collection Types

The library provides type aliases for collections of geohashes:

GeohashCollection

A generic iterable of geohash strings:

from pygeohash import GeohashCollection, mean

def calculate_center(geohashes: GeohashCollection) -> str:
    return mean(geohashes)
GeohashList

A concrete list of geohash strings:

from pygeohash import GeohashList

geohashes: GeohashList = ["9q9hwg", "9q9hwy", "9q9hwv"]

NumPy Integration

For users working with NumPy arrays, the library provides specialized array types:

GeohashArray

A NumPy array of geohash strings:

import numpy as np
from pygeohash import GeohashArray, encode

# Create a grid of coordinates
lats = np.array([37.7749, 37.7750, 37.7751])
lons = np.array([-122.4194, -122.4195, -122.4196])

# Convert to geohashes
geohashes: GeohashArray = np.array([
    encode(lat, lon) for lat, lon in zip(lats, lons)
])
LatitudeArray and LongitudeArray

NumPy arrays for latitude and longitude values:

from pygeohash import LatitudeArray, LongitudeArray

latitudes: LatitudeArray = np.array([37.7749, 37.7750, 37.7751])
longitudes: LongitudeArray = np.array([-122.4194, -122.4195, -122.4196])

Pandas Integration

For users working with Pandas, the library provides specialized Series and DataFrame types:

GeohashSeries, LatitudeSeries, and LongitudeSeries

Pandas Series for geohash strings and coordinates:

import pandas as pd
from pygeohash import GeohashSeries, LatitudeSeries, LongitudeSeries

geohashes: GeohashSeries = pd.Series(["9q9hwg", "9q9hwy", "9q9hwv"])
latitudes: LatitudeSeries = pd.Series([37.7749, 37.7750, 37.7751])
longitudes: LongitudeSeries = pd.Series([-122.4194, -122.4195, -122.4196])
GeohashDataFrame

A typed DataFrame with geohash-related columns:

from pygeohash import GeohashDataFrame

# Create a DataFrame with typed columns
df = GeohashDataFrame({
    'geohash': ["9q9hwg", "9q9hwy", "9q9hwv"],
    'latitude': [37.7749, 37.7750, 37.7751],
    'longitude': [-122.4194, -122.4195, -122.4196]
})

# Type checking will ensure these columns exist and have correct types
print(df.geohash)  # GeohashSeries
print(df.latitude)  # LatitudeSeries
print(df.longitude)  # LongitudeSeries

Utility Types

The library also includes utility types for specific purposes:

Direction

A literal type for cardinal directions:

from pygeohash import Direction, get_adjacent

def get_neighbor(geohash: str, dir: Direction) -> str:
    return get_adjacent(geohash, dir)  # dir must be "right", "left", "top", or "bottom"
GeohashPrecision

A type representing valid geohash precision values:

from pygeohash import GeohashPrecision, encode

def create_geohash(lat: float, lon: float, prec: GeohashPrecision = 6) -> str:
    return encode(lat, lon, prec)  # prec must be between 1 and 12

Type Safety and Fallbacks

The library’s type system is designed to be both helpful and unobtrusive:

  1. During development and type checking: - Full type information is available for IDE support - Type checkers will catch type-related errors

  2. At runtime: - If NumPy/Pandas are not available, types fall back to standard Python types - No runtime overhead or dependencies are added

Example: Type-Safe Function

Here’s an example of how to write a type-safe function that works with different input types:

from typing import Union
from pygeohash import (
    GeohashCollection, GeohashArray, GeohashSeries,
    LatitudeArray, LongitudeArray,
    encode
)

def process_coordinates(
    lats: Union[LatitudeArray, list[float]],
    lons: Union[LongitudeArray, list[float]],
    precision: GeohashPrecision = 6
) -> GeohashCollection:
    """Process coordinates and return geohashes.

    Works with both NumPy arrays and Python lists.
    """
    return [encode(lat, lon, precision) for lat, lon in zip(lats, lons)]

This type system helps catch errors early, provides better IDE support, and makes the code more maintainable while remaining flexible for different use cases.