Python SDK for jpzip — Japanese postal-code lookup served from a static CDN.
Project description
jpzip
Python SDK for jpzip — a free, unlimited Japanese postal code (郵便番号) API. 日本の全郵便番号 120,677 件を CDN 配信 JSON から引く Python SDK (sync + async)。
English | 日本語
jpzip looks up Japanese postal codes (郵便番号) from jpzip.nadai.dev,
a CDN-hosted dataset built from Japan Post's KEN_ALL.csv and KEN_ALL_ROME.csv
normalized to JSON. No registration, no rate limits, no API key.
- 🇯🇵 Complete dataset — 120,677 entries with kanji, kana, romaji, and government codes (JIS X 0401 / 総務省地方公共団体コード)
- ⚡️ Fast — L1 LRU + optional L2 persistent cache;
preload("all")to serve lookups without per-request network round-trips - 🔀 Sync + async —
JpzipClient(httpx.Client) andAsyncJpzipClient(httpx.AsyncClient) share the same API surface - 🛡️ Resilient — 3-attempt retry with exponential backoff on 5xx / network failures
- 🐍 Typed — frozen dataclasses,
Protocol-based cache contract, fully type-hinted - 🆓 Free forever — backed by Cloudflare Pages' free tier (no billing axis exists)
- 🔌 Drop-in — same API surface across every jpzip SDK
Requirements
- Python 3.10+
- One runtime dependency:
httpx(>=0.27) — powers both the sync and async clients
Install
pip install jpzip
Quick Start
Sync
import jpzip
entry = jpzip.lookup("2310017")
if entry is None:
print("not found")
else:
print(entry.prefecture, entry.city, entry.towns[0].town)
# Output: 神奈川県 横浜市中区 港町
Romaji and government codes are included on the same entry:
print(entry.prefecture_roma, entry.city_roma, entry.towns[0].roma)
# Output: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
print(entry.prefecture_code, entry.city_code)
# Output: 14 14104
Async
import asyncio
from jpzip import AsyncJpzipClient
async def main() -> None:
async with AsyncJpzipClient() as client:
entry = await client.lookup("2310017")
if entry is not None:
print(entry.prefecture, entry.city, entry.towns[0].town)
asyncio.run(main())
Use Cases
Zipcode lookup HTTP endpoint (FastAPI)
from fastapi import FastAPI, HTTPException
from jpzip import AsyncJpzipClient
app = FastAPI()
client = AsyncJpzipClient()
@app.on_event("shutdown")
async def _shutdown() -> None:
await client.aclose()
@app.get("/api/zipcode/{code}")
async def zipcode(code: str) -> dict:
entry = await client.lookup(code)
if entry is None:
raise HTTPException(status_code=404, detail="not found")
return {
"prefecture": entry.prefecture,
"city": entry.city,
"towns": [t.town for t in entry.towns],
"city_code": entry.city_code,
}
Batch validation
import jpzip
all_entries = jpzip.lookup_all() # entire dataset in memory (~37 MiB JSON)
for code in csv_zipcodes:
if code not in all_entries:
print(f"invalid zipcode: {code}")
Serve lookups from cache (BYO L2 backend)
The dataset is partitioned into 948 three-digit prefix buckets. The default
L1 (100 entries) keeps the hottest buckets; to cache the whole dataset, pair
preload("all") with an L2 cache or raise memory_cache_size above 948.
from jpzip import JpzipClient, Cache
class FileCache:
"""Any object structurally matching jpzip.Cache works (Protocol)."""
def get(self, key: str) -> bytes | None: ...
def set(self, key: str, value: bytes) -> None: ...
def delete(self, key: str) -> None: ...
def clear(self) -> None: ...
with JpzipClient(memory_cache_size=1024, cache=FileCache()) as client:
client.preload("all")
# Subsequent lookups are served from L1/L2 without hitting the network.
entry = client.lookup("2310017")
API Reference
Module-level shortcuts (sync, share a default JpzipClient)
| Function | Description |
|---|---|
lookup(zipcode) |
Look up a single 7-digit zipcode. Returns None if not found or malformed (no network call for malformed input). |
lookup_group(prefix) |
Look up by 1-, 2-, or 3-digit prefix. 1-digit fetches /g/{d}.json; 3-digit fetches /p/{ddd}.json; 2-digit fans out into 10 parallel 3-digit fetches and merges. Raises ValueError on a non-numeric / >3-digit prefix. |
lookup_all() |
Fetch entire dataset (120k entries, ~37 MiB) in parallel across /g/0..9.json. |
get_meta() |
Dataset version, generated-at, per-prefecture counts, spec version. Result is cached until refresh(). |
preload(scope) |
Warm L1 (and L2 when configured) for "all" or a specific prefix. |
is_valid_zipcode(s) |
Pure syntax check (^\d{7}$) — no network. |
JpzipClient (sync, advanced)
Instantiate directly when you need L2 caching, a custom httpx.Client, an alternate base URL, or multiple isolated caches:
from jpzip import JpzipClient
with JpzipClient(
base_url="https://jpzip.nadai.dev",
http_client=None, # provide your own httpx.Client to share pools
memory_cache_size=200, # L1 capacity in prefix buckets, default 100
cache=my_cache, # optional L2 (Cache protocol)
timeout=30.0,
on_spec_mismatch=lambda expected, received: print(
f"jpzip spec mismatch: SDK={expected} server={received}"
),
) as client:
entry = client.lookup("2310017")
JpzipClient exposes lookup / lookup_group / lookup_all / get_meta / preload plus:
| Method | Description |
|---|---|
client.refresh() |
Wipe L1 (and L2 when configured) and forget the cached meta. |
client.close() |
Close the owned httpx.Client. Use the context manager (with) to do this automatically. |
When get_meta() observes that /meta.json's version has changed since the last successful fetch, L1 and L2 are cleared automatically — call get_meta() periodically to pick up dataset rollovers.
AsyncJpzipClient (async)
Same constructor surface and same methods, but async:
import asyncio
from jpzip import AsyncJpzipClient
async def main() -> None:
async with AsyncJpzipClient(memory_cache_size=200) as client:
entry = await client.lookup("2310017")
meta = await client.get_meta()
await client.preload("231")
await client.refresh()
asyncio.run(main())
The async client accepts an AsyncCache (async methods) instead of Cache, and an httpx.AsyncClient instead of httpx.Client. Use await client.aclose() (or async with) for cleanup.
Errors
ValueError— raised bylookup_group/preloadwhen the prefix isn't 1–3 digits.RuntimeError— raised on non-404 4xx responses, or after exhausting retries on 5xx / network failures. Wraps the underlyinghttpx.HTTPErroron transport-level failures.- Network failures and 5xx responses are retried up to 3 attempts (initial + 2 retries) with exponential backoff sleeps of 400ms and 800ms. 404 responses yield
Noneimmediately without retrying. Other 4xx responses are raised immediately.
Cache / AsyncCache protocols
Bring your own L2 backend (file, SQLite, Redis, S3, etc.):
from typing import Protocol
class Cache(Protocol):
def get(self, key: str) -> bytes | None: ...
def set(self, key: str, value: bytes) -> None: ...
def delete(self, key: str) -> None: ...
def clear(self) -> None: ...
class AsyncCache(Protocol):
async def get(self, key: str) -> bytes | None: ...
async def set(self, key: str, value: bytes) -> None: ...
async def delete(self, key: str) -> None: ...
async def clear(self) -> None: ...
Both are @runtime_checkable Protocols — no inheritance required, just structural matching. Keys are the full prefix-bucket URLs (e.g. https://jpzip.nadai.dev/p/231.json); values are raw JSON bytes.
Dataclasses
ZipcodeEntry, Town, Meta, and Endpoints are frozen dataclasses (slots=True). All fields are typed and stable — see src/jpzip/_types.py.
Why jpzip-python?
| jpzip-python | posuto | pgeocode | |
|---|---|---|---|
Romaji (Yokohama Shi) |
✅ | ❌ (explicitly dropped) | ⚠️ romaji-only place names |
| Government codes (JIS / 総務省) | ✅ | ❌ | ❌ |
| No bundled CSV / DB in the wheel | ✅ (CDN-served) | ❌ (embeds SQLite) | ❌ (downloads CSV on first use) |
| Monthly updates | ✅ Auto | ✅ Monthly releases | ⚠️ GeoNames cadence |
| Sync and async client | ✅ | ❌ sync only | ❌ sync only |
Offline after preload("all") |
✅ | ✅ (always) | ✅ (always) |
| Rate-limit-free | ✅ | ✅ | ✅ |
| L1 + pluggable L2 cache | ✅ | n/a | n/a |
| Wheel size | KB (no embedded data) | MB (embedded SQLite) | depends on GeoNames CSV |
Other Languages
Same API surface across all SDKs:
Go · TypeScript · Rust · Ruby · PHP · Swift · Dart
Resources
- Website — https://jpzip.nadai.dev
- Protocol spec — jpzip/spec
- Data ETL — jpzip/data
- MCP server — jpzip/mcp — use jpzip from Claude / ChatGPT / Cursor
Keywords
japanese postal code, japan zipcode, 郵便番号, KEN_ALL, KEN_ALL_ROME, address validation, japan address api, postal code lookup python, python japanese address, async postal code, fastapi zipcode, httpx japanese address, JIS X 0401, 総務省地方公共団体コード
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jpzip-0.1.2.tar.gz.
File metadata
- Download URL: jpzip-0.1.2.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a89a20a0cc783fc17fafbee415ed10fa092e761621f00bbdb72691d7ba54ea7b
|
|
| MD5 |
fbc7744f112847a5259ee364e2624121
|
|
| BLAKE2b-256 |
cd2384aaaa897aadb3d179eab54d9e3e52fbc73f8bac250d4507718c33af20c9
|
Provenance
The following attestation bundles were made for jpzip-0.1.2.tar.gz:
Publisher:
publish.yml on jpzip/python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jpzip-0.1.2.tar.gz -
Subject digest:
a89a20a0cc783fc17fafbee415ed10fa092e761621f00bbdb72691d7ba54ea7b - Sigstore transparency entry: 1553397868
- Sigstore integration time:
-
Permalink:
jpzip/python@7c43735183d6de594c20de775d169e95e6b38110 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/jpzip
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7c43735183d6de594c20de775d169e95e6b38110 -
Trigger Event:
push
-
Statement type:
File details
Details for the file jpzip-0.1.2-py3-none-any.whl.
File metadata
- Download URL: jpzip-0.1.2-py3-none-any.whl
- Upload date:
- Size: 15.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
845655ca519d772731ff6eeffceb02d834d00145cdaddfb9fc7acc399dbaa5e6
|
|
| MD5 |
f0b627bd7a137aa67ff1a705a234713f
|
|
| BLAKE2b-256 |
fe507e9bb776659759a6a5939305c7db42570f85aba2173ea7a5854f23bda633
|
Provenance
The following attestation bundles were made for jpzip-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on jpzip/python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jpzip-0.1.2-py3-none-any.whl -
Subject digest:
845655ca519d772731ff6eeffceb02d834d00145cdaddfb9fc7acc399dbaa5e6 - Sigstore transparency entry: 1553397901
- Sigstore integration time:
-
Permalink:
jpzip/python@7c43735183d6de594c20de775d169e95e6b38110 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/jpzip
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7c43735183d6de594c20de775d169e95e6b38110 -
Trigger Event:
push
-
Statement type: