LTADataMallpy
A Python wrapper for the LTA DataMall API, providing typed, easy-to-use access to Singapore's public transport and traffic data: buses, trains, taxis, roads, active mobility, and geospatial layers.
Built on httpx, it ships with automatic pagination for endpoints returning more than 500 records, a persistent connection pool, and descriptive custom exceptions.
Features
- Single entry point:
DataMallwithtransport,traffic, andgeospatialnamespaces - Typed responses: every endpoint returns validated pydantic models (
LtaResult[T]for lists), not raw dicts - Automatic pagination (
$skiphandling) for long endpoints - Persistent
httpx.Clientwith a 30s timeout and explicitclose_client() - Custom exceptions with response status code, message, and timestamp
- Python
>=3.10(X | Nonesyntax, built-in generics), fully typed (py.typed-style annotations throughout)
Installation
From PyPI
pip install lta-api-py
Getting an API key
- Register at the LTA DataMall portal.
- Create an account and request access to an API key.
- Use the key as the
api_keyargument (or load it from an environment variable /.envfile).
Quick start
from LTADataMallpy import DataMall
import os
dm = DataMall(api_key=os.getenv("LTA_API_KEY"))
# Transport data — typed pydantic models, not dicts
arrivals = dm.transport.bus.arrival.get_bus_arrival(stopcode="83111")
for service in arrivals.services:
print(service.service_no, service.next_bus.estimated_arrival)
# Traffic data
speed_bands = dm.traffic.roads.get_traffic_speed_bands()
for band in speed_bands.value:
print(band.road_name, band.speed_band)
Always close the client when you are done to release the connection pool:
from LTADataMallpy.helpers import close_client
# ... your requests ...
close_client()
API reference
Everything is imported from the top-level package LTADataMallpy.
Main entry point: DataMall
from LTADataMallpy import DataMall
dm = DataMall(api_key: str, accept: str | None = None)
Raises ValueError if api_key is empty or missing. Exposes three namespaces:
| Attribute | Type | Description |
|---|---|---|
dm.transport |
Transport |
Bus, train, taxi, active mobility |
dm.traffic |
Traffic |
Roads and infrastructure |
dm.geospatial |
Geospatial |
Geospatial whole-island layers |
Transport
from LTADataMallpy import DataMall
dm = DataMall(API_KEY)
t = dm.transport
| Attribute | Class | Description |
|---|---|---|
t.bus |
Bus |
Bus services/routes |
t.train |
Train |
Train stations/services |
t.taxi |
Taxi |
Taxi availability |
t.active_mobility |
ActiveMobility |
Bicycle parking |
Traffic
dm = DataMall(API_KEY)
tr = dm.traffic
| Attribute | Class | Description |
|---|---|---|
tr.roads |
Roads |
Road/traffic data |
tr.infra |
Infra |
Infrastructure data |
Geospatial
dm = DataMall(API_KEY)
G = dm.geospatial
G.get_geo_layer(id="LayerName")
LTA DataMall geospatial layers require a Token in the request header in addition to AccountKey.
Pass the token as the accept argument (it is forwarded as the accept header value):
dm = DataMall(api_key=API_KEY, accept=YOUR_GEOSPATIAL_TOKEN)
data = dm.geospatial.get_geo_layer(id="factoryarea")
Classes to import directly
Each underlying class is importable on its own, taking (api_key, accept=None) unless noted:
from LTADataMallpy.Bus import Bus
from LTADataMallpy.Train import Train
from LTADataMallpy.Taxi import Taxi
from LTADataMallpy.Roads import Roads
from LTADataMallpy.Infra import Infra
from LTADataMallpy.ActiveMobility import ActiveMobility
from LTADataMallpy.Geo import Geospatial
from LTADataMallpy.subclasses import Transport, Traffic
Methods
Bus
| Class | Method | Notes |
|---|---|---|
Bus |
— | Aggregates the four sub-objects |
Bus.arrival |
(BusArrival) |
|
BusArrival |
get_bus_arrival(stopcode, serviceno=None) |
BusStopCode required |
Bus.services |
(BusServices) |
|
BusServices |
get_bus_services(serviceno=None) |
Paginated |
Bus.routes |
(BusRoutes) |
|
BusRoutes |
get_bus_routes() |
Paginated |
BusRoutes |
get_planned_routes() |
|
Bus.stops |
(BusStops) |
|
BusStops |
get_bus_stops(stopcode=None) |
Paginated |
BusStops |
get_pvolume_bus_stop(date=None, origin_destination=None) |
Bus or OD-Bus volume |
Train
| Class | Method | Notes |
|---|---|---|
Train |
station, service sub-objects |
|
TrainStation |
get_pvolume_train_station(date=None, origin_destination=None) |
Train or OD-Train volume |
TrainStation |
get_maintenance() |
|
TrainService |
get_train_service_alerts() |
|
TrainService |
get_crowd_density(line) |
TrainLine required |
TrainService |
get_crowd_density_forecast(line) |
TrainLine required |
TrainService |
get_gtfs_train_schedule() |
|
TrainService |
get_gtfs_train_service_real_time() |
|
TrainService |
get_gtfs_train_trip_update() |
Taxi
| Class | Method | Notes |
|---|---|---|
Taxi |
get_taxi_availability() |
Paginated |
Taxi |
get_taxi_stands() |
Roads (traffic)
| Class | Method | Notes |
|---|---|---|
Roads |
get_est_travel_times() |
|
Roads |
get_carpark_availability() |
Paginated |
Roads |
get_road_openings() |
|
Roads |
get_road_works() |
Paginated |
Roads |
get_traffic_incidents() |
|
Roads |
get_traffic_speed_bands() |
|
Roads |
get_traffic_flows() |
|
Roads |
get_flood_alerts() |
Infra (traffic)
| Class | Method | Notes |
|---|---|---|
Infra |
get_faulty_lights() |
|
Infra |
get_traffic_images() |
Paginated |
Infra |
get_vms_emas() |
|
Infra |
get_ev_charge_points(postalcode) |
PostalCode required |
Infra |
get_ev_charge_points_batch() |
ActiveMobility
| Class | Method | Notes |
|---|---|---|
ActiveMobility |
get_bicycle_parking(lat, long, dist=None) |
Lat/Long required; Paginated |
Geospatial
| Class | Method | Notes |
|---|---|---|
Geospatial |
get_geo_layer(id) |
ID required; needs accept token |
Helpers
Low-level helpers live in LTADataMallpy.helpers and can be used for custom endpoint calls:
from LTADataMallpy.helpers import (
build_headers, # build the AccountKey/accept header dict from an API key
build_url, # join an endpoint to the DataMall base URL
make_request, # GET one page; raises custom errors in the helpers module
make_paginated_request, # fetch all pages, combining "value" lists
create_client, # lazily create/reuse the persistent httpx.Client
close_client, # close the persistent client (self-heals on next use)
)
Example of a custom call:
from LTADataMallpy.helpers import build_headers, build_url, make_request
headers = build_headers(API_KEY)
url = build_url("SomeEndpoint")
data = make_request(headers, url, params={"SomeParam": "value"})
Persistent client
- The shared
httpx.Clientis created lazily on the first request (withtimeout=30.0) and reused for every call, keeping TCP/TLS connections alive. - Call
close_client()at the end of a script to free resources. It is safe to call mid-run: the next request transparently recreates the client. httpx.Clientis thread-safe, so the client can be shared across threads.
Pagination
Endpoints marked Paginated above go through make_paginated_request, which automatically walks $skip in 500-record pages and merges every page's value list into a single response before it is validated into the model.
Error handling and debugging
make_request maps known HTTP statuses to descriptive exceptions (all in LTADataMallpy.helpers, subclasses of DataMallError):
| HTTP status | Exception | Meaning |
|---|---|---|
| 401 / 403 | DataMallPermissionError |
Invalid/missing API key |
| 404 | DataMallNotFoundError |
Requested data not available for this account |
| 500 / 502 / 503 | DataMallBackendError |
LTA backend server error |
| 429 | DataMallRateLimitError |
Rate limit hit; slow your request frequency |
Every DataMallError exposes .status_code and .detail (the API's error message when parseable) and stringifies as "{status_code} {detail}". Any other status passes through httpx.Response.raise_for_status().
from LTADataMallpy import DataMall
from LTADataMallpy.helpers import DataMallPermissionError, DataMallRateLimitError
dm = DataMall(api_key="invalid-key")
try:
dm.transport.bus.arrival.get_bus_arrival("83111")
except DataMallPermissionError as e:
print("Bad key:", e.status_code, e.detail)
except DataMallRateLimitError as e:
print("Slow down:", e.status_code, e.detail)
Debugging tips
- 429 / rate limit: LTA enforces per-minute request quotas. Add backoff, or cache responses you fetch repeatedly.
- Timeout: the client uses a 30-second timeout; paginated calls to big datasets (e.g.
BusRoutes,BusServices) can be slow on a cold connection. - Check connectivity: confirm the key is active in the DataMall portal and that your network reaches
datamall2.mytransport.sg. .env/ secrets: load your key from an environment variable rather than hard-coding it. The.envfile is git-ignored.
Development
uv sync --group dev # install with dev extras (pytest, python-dotenv)
uv run pytest tests/ # full suite; hits the live API, needs LTADATAMALL_API_KEY in .env
uv build # build sdist + wheel into dist/
uvx twine check dist/* # validate package metadata
Tests read LTADATAMALL_API_KEY from a .env file (git-ignored) or the environment; without it they skip. Requests are paced ~3s apart to respect LTA rate limits.
License
Apache License 2.0. See LICENSE.
Links
Metadata
Release files for lta-api-py 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| lta_api_py-0.2.0.tar.gz | 14.1 kB | With provenance | Details for lta_api_py-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| lta_api_py-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for lta_api_py-0.2.0-py3-none-any.whl |
Total release size: 32.2 kB
Release files / lta_api_py-0.2.0.tar.gz
| Download URL | lta_api_py-0.2.0.tar.gz |
|---|---|
| Size | 14.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
daba981eef29ea92a3277869904c3900ce0ae771de2cefc12afbd8108849ed6d
|
|
BLAKE2b-256 checksum How to use checksums |
c2977827fa9819bd1a69995022e0fc19abfc15bfe0b7f0f1bcc9945823a32c20
|
| 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 Oct 21, 2026.
Transparency logRelease files / lta_api_py-0.2.0-py3-none-any.whl
| Download URL | lta_api_py-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8c32eb74849e168cbbb1c26d05d6e366a82d906c74908ddae5f4937c65ae4c0c
|
|
BLAKE2b-256 checksum How to use checksums |
dd87fea334f300ec5c19fa20247fce015121181e6b1e9c21ac8cd70f5c5852d8
|
| 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 Oct 21, 2026.
Transparency logSecurity for lta-api-py 0.2.0
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.
Attestation for lta_api_py-0.2.0.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 Oct 21, 2026.
Transparency logA 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.