Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

pyxpo-location

CI PyPI Python License: MIT

Where the phone is: position, live updates, compass heading and address lookup for pyxpo apps, written in Python. The API follows expo-location. iOS uses Core Location. Android uses the platform LocationManager (no Google Play services), the accelerometer and magnetometer for the heading, and Geocoder for addresses.

The location module on Android and iOS: permission status, latitude and longitude, and a reverse-geocoded address

import pyxpo_location as Location

perm = await Location.request_foreground_permissions_async()
if perm["granted"]:
    loc = await Location.get_current_position_async(accuracy=Location.Accuracy.HIGH)
    print(loc["coords"]["latitude"], loc["coords"]["longitude"])

Features

  • Permissions with expo's response shape, plus which accuracy the user granted ("fine" or "coarse" on Android, "whenInUse" or "always" on iOS). A hook reads or asks on mount.
  • Current and last known position with six accuracy levels from about 3 km to the best the device can do.
  • Live updates with watch_position_async, tuned by accuracy, time_interval and distance_interval.
  • Compass heading, once or as a stream.
  • Geocoding both ways: an address to coordinates, and coordinates to a street address.
  • Provider status: are location services on, is GPS or network location available.
  • Honest limits: background updates and geofencing raise NotSupportedError, because pyxpo has no task manager yet.
  • Typed: ships py.typed. Every function has its expo spelling too (getCurrentPositionAsync, {"distanceInterval": 10}).

Contents

If you are... Start here
Showing "near me" once Quick start
Asking for permission Permissions
Following the user on a map Live updates
Building a compass Heading
Turning an address into a place (or back) Geocoding
Needing tracking with the app closed Not supported yet
Looking for exact signatures API reference · Errors
Seeing it in a real app Recipe: a "find stores near me" screen
Stuck Testing · Troubleshooting · FAQ

Platform support

Target What works
Android, API 24+ Position, watching, heading (sensors), geocoding (Geocoder), provider status. Works on phones without Google Play services.
iOS 15+ Position, watching, heading, geocoding, services check. The Simulator has a place you can set (Features > Location) but no compass.
Web page and desktop window Not supported. Every function raises NotSupportedError, except has_services_enabled_async(), which returns False.

Requires Python 3.9+ and pyxpo>=0.1.0a2.

Install

pip install pyxpo-location
[tool.pyxpo]
modules = ["location"]

# Optional: the text of the iOS permission prompts.
[tool.pyxpo.module_options.location]
locationWhenInUsePermission = "Show places near you."

Rebuild the app after adding the module (pyxpo run android or pyxpo run ios). On Android the module adds ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION and marks the location hardware as optional.

Quick start

import pyxpo_location as Location
from pyxpo import Button, Text, View, component, use_async, use_state


@component
def Here():
    wanted, set_wanted = use_state(0)
    place, set_place = use_state("Not yet")

    async def locate():
        if wanted == 0:
            return
        perm = await Location.request_foreground_permissions_async()
        if not perm["granted"]:
            set_place("Permission denied")
            return
        loc = await Location.get_current_position_async(accuracy=Location.Accuracy.HIGH)
        c = loc["coords"]
        set_place("%.4f, %.4f" % (c["latitude"], c["longitude"]))

    use_async(locate, [wanted])
    return View(Text(place), Button("Locate me", lambda: set_wanted(wanted + 1)))

Tip: use_async(fn, deps) awaits a real async def when the dependencies change. A button that must start async work bumps a counter in state, and the counter is a dependency, as above.

Locate me on Android and iOS showing latitude, longitude, accuracy, altitude and a San Francisco address

Permissions

perm = await Location.get_foreground_permissions_async()       # does not ask
perm = await Location.request_foreground_permissions_async()   # asks if undetermined
status, request, get = Location.use_foreground_permissions()    # a hook

The response is expo's, {"status", "granted", "canAskAgain", "expires"}, plus:

Platform Extra key Values
iOS ios: {"scope"} "whenInUse", "always", "none"
Android android: {"accuracy"} "fine", "coarse", "none"

An Android user can allow only approximate location, so granted can be true with "coarse". Positions are then less precise.

Hook Meaning
use_foreground_permissions(get=True, request=False) (response, request, get). response is None until read. get=True reads on mount, request=True asks on mount. request and get are coroutines that also update the component.
use_background_permissions(...) The same for background location. Ask for the foreground permission first. Android needs ACCESS_BACKGROUND_LOCATION in [tool.pyxpo.android] permissions, and Android 11+ sends the user to Settings.

Current position

loc = await Location.get_current_position_async(accuracy=Location.Accuracy.BALANCED)
last = await Location.get_last_known_position_async(max_age=60_000, required_accuracy=100)

A location is {"coords": {"latitude", "longitude", "altitude", "accuracy", "altitudeAccuracy", "heading", "speed"}, "timestamp", "mocked"}. Values the device does not know are None. timestamp is milliseconds since 1970. mocked is Android only.

Accuracy Meaning
LOWEST (1) About 3 km
LOW (2) About 1 km
BALANCED (3) About 100 m. The default.
HIGH (4) About 10 m
HIGHEST (5) The best the device can do
BEST_FOR_NAVIGATION (6) The best, plus sensor data on iOS

Higher accuracy costs more battery. get_last_known_position_async returns None when there is no cached location, or it is older than max_age ms, or less exact than required_accuracy metres.

Live updates

sub = await Location.watch_position_async(
    {"accuracy": Location.Accuracy.BALANCED, "distanceInterval": 10},
    lambda loc: print(loc["coords"]),
)
...
sub.remove()
Option Meaning
accuracy An Accuracy.
time_interval Android only, minimum milliseconds between updates.
distance_interval Minimum metres moved between updates.

Pass error_handler to hear about failures. sub.remove() stops the updates and is safe to call twice. Start the watch from use_async (or a function that you stop in a cleanup) and remove it when the screen goes away, or it keeps draining the battery.

Heading

h = await Location.get_heading_async()      # {"trueHeading", "magHeading", "accuracy"}
sub = await Location.watch_heading_async(lambda h: print(h["magHeading"]))

Values are degrees. trueHeading is -1 until the location is known. A device without a compass, and the iOS Simulator, raise an error instead.

Geocoding

spots = await Location.geocode_async("1600 Amphitheatre Parkway, Mountain View")
places = await Location.reverse_geocode_async({"latitude": 48.8584, "longitude": 2.2945})

geocode_async returns [{"latitude", "longitude", "altitude", "accuracy"}]. reverse_geocode_async returns addresses with name, street, streetNumber, district, city, subregion, region, postalCode, country, isoCountryCode, timezone and formattedAddress. Both need a network. An empty address raises ValueError, and so does a latitude or longitude that is not a number or out of range.

Provider status

Function Returns
has_services_enabled_async() True when location services are on.
get_provider_status_async() locationServicesEnabled, backgroundModeEnabled, and on Android gpsAvailable, networkAvailable, passiveAvailable.
enable_network_provider_async() Android: opens the location settings when network location is off.

Not supported yet

start_location_updates_async, stop_location_updates_async, has_started_location_updates_async and the matching geofencing functions need a background task manager, which pyxpo does not have yet. They raise NotSupportedError, and is_background_location_available_async() returns False. Use watch_position_async while the app is open.

Errors

Error When What to do
NotSupportedError On the web page or desktop, or a background or geofencing function. Check platform() first, or catch it and show a manual place picker.
ModuleNotLinkedError (from pyxpo.module_api) Built without "location" in modules. Add it and rebuild.
ValueError Bad accuracy, negative max_age, empty address, or bad coordinates. Fix the argument.
TypeError The watch callback is not callable. Pass a function.
A native error (a plain exception with a message) No permission, services off, or no fix in time. Ask for permission, check has_services_enabled_async(), and offer a retry.

Web and desktop

Nothing is available there. has_services_enabled_async() returns False, and the other functions raise NotSupportedError. For a web build, let the user type an address and use your own geocoding service.

Recipe: a "find stores near me" screen

Ask once, show the place, and fall back to the last known position when a fresh fix is slow.

import pyxpo_location as Location
from pyxpo import Button, Text, View, component, use_async, use_state


@component
def NearMe():
    tries, set_tries = use_state(1)
    text, set_text = use_state("Looking...")

    async def find():
        perm = await Location.request_foreground_permissions_async()
        if not perm["granted"]:
            set_text("Allow location to see stores near you.")
            return
        try:
            loc = await Location.get_current_position_async(accuracy=Location.Accuracy.BALANCED)
        except Exception:
            loc = await Location.get_last_known_position_async(max_age=10 * 60_000)
        if loc is None:
            set_text("No position yet. Is location on?")
            return
        c = loc["coords"]
        where = await Location.reverse_geocode_async(
            {"latitude": c["latitude"], "longitude": c["longitude"]})
        city = where[0].get("city") if where else None
        set_text("Stores near %s" % (city or "you"))

    use_async(find, [tries])
    return View(Text(text), Button("Try again", lambda: set_tries(tries + 1)))

Production checklist

  • Ask for location after the user picks a feature that needs it, not at launch, and explain why first.
  • locationWhenInUsePermission says why you need it, in the user's words.
  • You handle denied, approximate-only (Android "coarse") and services-off cases.
  • Accuracy is no higher than you need. BALANCED is enough for "near me".
  • Every watch_* subscription is removed when the screen goes away.
  • You do not promise tracking with the app closed.
  • Your privacy policy says what you do with the position.
  • You tested on a real phone, not only an emulator with a fixed point.

Testing

Run unit tests on the desktop host by replacing the native call:

import asyncio

import pyxpo_location as Location


def test_current_position(monkeypatch):
    async def fake_call(*args, **kwargs):
        return {"coords": {"latitude": 52.52, "longitude": 13.4, "accuracy": 5}, "timestamp": 0}

    monkeypatch.setattr(Location, "_native", lambda: None)
    monkeypatch.setattr(Location, "call", fake_call)

    loc = asyncio.run(Location.get_current_position_async(accuracy=Location.Accuracy.HIGH))
    assert loc["coords"]["latitude"] == 52.52

Argument checks need no fake: Location.get_current_position_async(accuracy=9) raises ValueError before any native call.

Troubleshooting

Symptom Cause and fix
NotSupportedError on desktop or web Location is phone only.
The Simulator or emulator shows a fixed place Set it in the iOS Simulator (Features > Location) or the Android emulator (Extended controls > Location).
Position takes a long time indoors Lower the accuracy, or fall back to get_last_known_position_async.
granted is true but the place is rough An Android user chose approximate location. Check perm["android"]["accuracy"].
get_heading_async raises The device has no compass (and the iOS Simulator has none).
No prompt appears The permission was denied before and canAskAgain is False. Send the user to Settings.
trueHeading is -1 The location is not known yet. Use magHeading.
ModuleNotLinkedError Add "location" to modules and rebuild.

FAQ

Does it need Google Play services? No. Android uses the platform LocationManager.

Can it track in the background? Not yet. See Not supported yet.

Can I get the address of a place? Yes, with reverse_geocode_async.

Is the heading true north? trueHeading is, once the location is known. magHeading is magnetic north.

Why does watch_position_async take both a dict and keywords? The dict is expo's shape. Keywords are the Python shape. Both work.

API reference

Permissions

Function Returns
get_foreground_permissions_async() / request_foreground_permissions_async() The permission response.
get_background_permissions_async() / request_background_permissions_async() The same, for background location.
use_foreground_permissions(get=True, request=False) / use_background_permissions(...) (response, request, get).

Position, heading and geocoding

Function Returns
get_current_position_async(options=None, **kw) A location.
get_last_known_position_async(options=None, **kw) A location or None. Options max_age, required_accuracy.
watch_position_async(options=None, callback=None, error_handler=None, **kw) LocationSubscription.
get_heading_async() / watch_heading_async(callback, error_handler=None) A heading, or LocationSubscription.
geocode_async(address) A list of coordinates.
reverse_geocode_async(location) A list of addresses.
has_services_enabled_async() / get_provider_status_async() / enable_network_provider_async() Provider status.
is_background_location_available_async() Always False.
start_location_updates_async, start_geofencing_async and their stop and has-started functions Raise NotSupportedError.

Types

Name Description
Accuracy (also LocationAccuracy) LOWEST to BEST_FOR_NAVIGATION, 1 to 6.
ActivityType iOS activity hint: OTHER, AUTOMOTIVE_NAVIGATION, FITNESS, OTHER_NAVIGATION, AIRBORNE.
GeofencingEventType, GeofencingRegionState Defined for compatibility, unused while geofencing is unsupported.
PermissionStatus GRANTED, DENIED, UNDETERMINED.
LocationSubscription Has remove().

License

MIT

Metadata

Release files for pyxpo-location 0.1.0a2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyxpo-location 0.1.0a2
File Size Uploaded Provenance Actions
pyxpo_location-0.1.0a2.tar.gz 29.6 kB With provenance Details for pyxpo_location-0.1.0a2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyxpo-location 0.1.0a2
File Interpreter ABI Platform Actions
pyxpo_location-0.1.0a2-py3-none-any.whl Python 3 none any Details for pyxpo_location-0.1.0a2-py3-none-any.whl

Total release size: 53.9 kB

Release files / pyxpo_location-0.1.0a2.tar.gz

Download URL pyxpo_location-0.1.0a2.tar.gz
Size 29.6 kB
Tags Source
SHA-256 checksum
How to use checksums
3785a41af131d215b5437d6712de650a12357719fe885209481b6326da08447a
BLAKE2b-256 checksum
How to use checksums
a8d301ee15239876879abf5ca0a99507fbbf68135743f76a629886cc869cec5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / pyxpo_location-0.1.0a2-py3-none-any.whl

Download URL pyxpo_location-0.1.0a2-py3-none-any.whl
Size 24.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2e292b16a20967b207801eeae401fd8b30d3acd6cd94c2d7715d5c8bf03000bc
BLAKE2b-256 checksum
How to use checksums
a89036eb7729c2b5996d1d33bde563b53f4ed31662df811e56af2ab6ceb19d07
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for pyxpo-location 0.1.0a2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pyxpo_location-0.1.0a2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page