Skip to main content

countrystatecity-api

PyPI Python Version License Type Checked

The official Python client for the Country State City API. Continuously updated geographic data, with sync and async access, typed payloads, and structured errors.

60 seconds to your first request

pip install countrystatecity-api

Create a free API key (no card required), then:

export CSC_API_KEY="your-api-key"
from countrystatecity import CountryStateCity

csc = CountryStateCity()          # reads CSC_API_KEY

india = csc.get_country("IN")
print(india["emoji"], india["name"], "-", india["capital"])

for state in csc.get_states_of_country("IN"):
    print(state["iso2"], state["name"])

That is the whole setup. Reuse one client for the life of your process; it holds a connection pool.

Async

The same surface, without blocking the event loop:

import asyncio
from countrystatecity import AsyncCountryStateCity

async def main() -> None:
    async with AsyncCountryStateCity() as csc:
        country, states = await asyncio.gather(
            csc.get_country("IN"),
            csc.get_states_of_country("IN"),
        )
        print(country["name"], len(states), "states")

asyncio.run(main())

Both clients take the same arguments, expose the same method names, and return the same payloads. A test asserts their signatures stay identical.

Handling failures

Every failure is a subclass of CountryStateCityError, so one except clause contains the client. Branch further when you want to act on the reason:

from countrystatecity import (
    CountryStateCity,
    APITimeoutError,
    NotFoundError,
    PermissionDeniedError,
    RateLimitError,
)

csc = CountryStateCity(timeout=10.0)

try:
    cities = csc.get_cities_of_state("IN", "MH", q="pune")
except NotFoundError:
    cities = []
except PermissionDeniedError as exc:
    # Endpoint or query feature not included in this plan.
    print(f"{exc.feature} requires an upgrade: {exc.upgrade_url}")
except RateLimitError as exc:
    print(f"{exc.period} limit of {exc.limit} reached on the {exc.tier} plan")
    print(f"Resets at {exc.reset_at}")     # ISO 8601 UTC, or None
    print(f"Raise it at {exc.upgrade_url}")
except APITimeoutError:
    ...  # retry on your own schedule; see "No implicit retries" below
Exception Raised for
ConfigurationError Missing/blank key, bad base URL, non-finite timeout — at construction
ValidationError An argument the API would reject; raised before the request is sent
APIConnectionError DNS, TLS, refused connection, truncated response
APITimeoutError Request exceeded the timeout (subclass of APIConnectionError)
BadRequestError 400
AuthenticationError 401 — key missing, malformed, or unknown
PermissionDeniedError 403 — plan restriction, or a domain/IP allow-list block
NotFoundError 404
RateLimitError 429 — daily or monthly quota exhausted
ServerError 5xx
APIResponseError A 2xx body that was not valid JSON

APIStatusError (the parent of the status classes) always carries .status_code, .details, .method, and .url. .details normalises both error envelopes the API emits, so feature, upgradeUrl, tier, limit, and period are readable regardless of which layer rejected the request.

Plan and quota headers

Every /v1 response reports your tier and usage. Reach them through the low-level request() method, which returns the payload and its metadata:

response = csc.request("/countries")

print(response.meta.plan)                 # 'supporter'
print(response.meta.daily.used,
      response.meta.daily.limit,
      response.meta.daily.remaining)      # 42 1000 958
print(response.meta.cache)                # 'HIT' or 'MISS'
print(response.meta.etag)

countries = response.data

request() doubles as an escape hatch for any endpoint this version does not wrap yet.

For unlimited plans, daily.unlimited is True and limit/remaining are None. On a 401 or 429 the API rejects the request before setting those headers, so metadata is empty there — a RateLimitError's .limit, .period, .tier, and .reset_at come from the response body instead.

Search, field selection, and sorting

Paid plans add server-side query features. Ask for less data and the responses get much smaller:

# Inline search
csc.get_cities_of_country("IN", q="pune")

# Only the fields you use
csc.get_countries(fields=["id", "name", "iso2", "emoji"])

# Server-side ordering
csc.get_countries(sort="population:desc")

# Typo-tolerant search
csc.fuzzy_search("bangalor", entity="city", country="IN", limit=5)

fields and sort accept a comma-separated string or a list of strings. Requesting a feature your plan does not include raises PermissionDeniedError with the exact feature name and an upgrade URL.

Postcode lookup and search

Exact postcode lookup is free on every plan, including Community. A postcode can map to more than one locality, so it always returns a list -- never just the first match:

matches = csc.get_postcodes_by_code("IN", "110001")
for postcode in matches:
    print(postcode["locality_name"], postcode.get("state_code"))

Paginated listing and search adds filters, but requires a Supporter plan or above:

page = csc.get_postcodes_of_country("IN", q="110", state_code="DL", limit=50)
for postcode in page["data"]:
    print(postcode["code"], postcode.get("locality_name"))

if page["pagination"]["has_more"]:
    page = csc.get_postcodes_of_country(
        "IN", q="110", cursor=page["pagination"]["next_cursor"]
    )

cursor is opaque -- always pass back the exact value from page["pagination"]["next_cursor"], never one you construct yourself. There is no total count in the response; keep paging while has_more is True.

Both postcode methods accept only an ISO 3166-1 alpha-2 country code -- unlike every other country-scoped method on this client, ISO3 codes and numeric ids are not accepted here.

Compare plans to unlock search.

API reference

All methods issue one HTTP GET. country accepts an ISO 3166-1 alpha-2 code, an alpha-3 code, or a numeric country id -- except the postcode methods, which accept alpha-2 only.

Countries, states, cities

Method Endpoint
get_countries(q=, fields=, sort=) GET /countries
get_country(country, fields=) GET /countries/{ciso}
get_states(q=, fields=, sort=) GET /states
get_states_of_country(country, q=, fields=, sort=) GET /countries/{ciso}/states
get_state(country, state, fields=) GET /countries/{ciso}/states/{siso}
get_cities_of_country(country, q=, fields=, sort=) GET /countries/{ciso}/cities
get_cities_of_state(country, state, q=, fields=, sort=) GET /countries/{ciso}/states/{siso}/cities

Regions

Method Endpoint
get_regions(q=, fields=, sort=) GET /regions
get_region(region_id, fields=) GET /regions/{id}
get_subregions_of_region(region_id, q=, fields=, sort=) GET /regions/{id}/subregions
get_subregion(subregion_id, fields=) GET /subregions/{id}
get_countries_of_subregion(subregion_id, q=, fields=, sort=) GET /subregions/{id}/countries

Timezones, currencies, phone

Method Endpoint
get_timezone_of_country(country) GET /timezone/{ciso}
get_timezone_of_state(country, state) GET /timezone/{ciso}/{siso}
get_timezone_of_city(country, state, city_id) GET /timezone/{ciso}/{siso}/{city_id}
get_currencies(code=) GET /currency
get_currency_of_country(country) GET /currency/{ciso}
get_dial_codes(code=) GET /phone
get_dial_code_of_country(country) GET /phone/{ciso}
parse_phone_number(number) GET /phone/parse

ISO lookup and search

Method Endpoint
lookup_country_iso(iso2=, iso3=, numeric=) GET /iso/country
lookup_state_iso(iso) GET /iso/state
convert_country_code(value, from_format=, to_format=) GET /iso/country/convert
fuzzy_search(query, entity=, country=, limit=, threshold=) GET /search/fuzzy
request(path, params=) Any GET under the base URL

Postcodes

Method Endpoint
get_postcodes_by_code(country, code, fields=) GET /countries/{iso2}/postcodes/{code}
get_postcodes_of_country(country, q=, state_code=, city_id=, type=, limit=, cursor=, fields=) GET /countries/{iso2}/postcodes

Full endpoint reference: docs.countrystatecity.in · Try it live: playground.countrystatecity.in

Types

Payloads are plain dicts. countrystatecity.types describes their shape with TypedDicts — Country, State, City, Region, Subregion, TimezoneInfo, CurrencyInfo, DialCode, PhoneParsed, IsoCountry, IsoState, IsoConvert, FuzzyResult, Postcode, PostcodePagination, and PostcodeSearchResult.

Every field is declared optional, because which fields arrive depends on your plan's data-access level and on the fields parameter. total=False describes presence, not value: when a key is there, its declared type is what you get. The type checker therefore treats every key as possibly missing, id and name included. Read the ones outside your plan's guaranteed set with .get(), and assert the ones your plan does guarantee at your own boundary:

country = csc.get_country("IN")
name = country["name"]                     # present on every plan; a KeyError
                                           # here means the API changed
population = country.get("population")     # coordinates tier and above

Ids are strings. Every id, foreign id, population, and gdp is a 64-bit BIGINT in the API's database, and the API serialises those as JSON strings — a bigint does not survive a round trip through a JavaScript number. So country["id"] == "101", not 101. Convert with int() where you need arithmetic. level, area_sq_km, and match_score are ordinary numbers.

The package ships py.typed, so mypy and Pyright see these types with no stub package.

Behaviour worth knowing

Your key stays a secret. It is sent only in the X-CSCAPI-KEY header, never in a URL. It does not appear in repr(client), in exception messages, or in APIStatusError.url. Keep it in a server-side environment variable — never in browser code, a mobile app, or source control.

Request failures are safe to log. APIStatusError.url and transport error messages carry the scheme, host, and path only — the query string and fragment are dropped. Raw-path and query-parameter validation errors do not repeat the rejected input. Query values are your data: parse_phone_number() sends a phone number and q= sends a search term, and a failure should not put either into your logs. The request itself is unaffected; only what the exception records is trimmed.

The request() escape hatch stays under the base URL. //host, an embedded ? or #, and ./.. segments (including percent-encoded ones) are rejected with ValidationError, so a path built from untrusted input cannot walk out of /v1 and reach another route.

Nothing happens at import. Importing the package and constructing a client open no connections. The first request is the one you make.

No implicit retries. A silent retry would spend a second request from your quota without you asking. Add your own policy where you want one, and back off on RateLimitError — the free Community plan is a small daily allowance.

Finite timeouts. The default is 30 seconds per request. timeout=None is rejected: an unbounded read can wedge a worker forever.

Arguments are validated locally. Country codes, state codes, ids, search terms, field lists, and phone numbers are checked against the same rules the API enforces, so a malformed call raises ValidationError instead of spending a request on a guaranteed 400. Field names are checked for shape only — the API stays the authority on which columns exist for your plan.

No telemetry. The only thing this package reports about itself is a standard User-Agent string.

Offline packages

The countrystatecity-* packages are versioned offline snapshots — no network, no key, no quota. They suit development, tests, air-gapped builds, and anything that must be reproducible.

Package Contents
countrystatecity-countries Countries, states, cities
countrystatecity-timezones IANA timezones and conversion
countrystatecity-currencies Country/currency associations
countrystatecity-translations Country names in 19 languages
countrystatecity-phonecodes International dialing codes
countrystatecity-regions Regions and subregions
countrystatecity-postal-codes Postal/ZIP records and validation

Use this client instead when you need data that is current rather than pinned, server-side search and filtering, field-selected responses, fuzzy matching, managed availability, or support.

Migration guide · Compare plans

Requirements

Python 3.8+ and httpx, which provides both the sync and async transports.

License

ODbL-1.0 — see LICENSE. The geographic data this client retrieves comes from countries-states-cities-database.


Made with ❤️ by dr5hn

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

countrystatecity_api-0.2.1.tar.gz (44.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

countrystatecity_api-0.2.1-py3-none-any.whl (47.1 kB view details)

Uploaded Python 3

File details

Details for the file countrystatecity_api-0.2.1.tar.gz.

File metadata

  • Download URL: countrystatecity_api-0.2.1.tar.gz
  • Upload date:
  • Size: 44.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.14

File hashes

Hashes for countrystatecity_api-0.2.1.tar.gz
Algorithm Hash digest
SHA256 044866bb574ae8562014008f13cda4e8cd91632d31a73be29ca33cda255421da
MD5 1fe5b56c17cf03878ca35e478532ae6b
BLAKE2b-256 2c56062d3d638b6b847156890f5d35b3667cd8bd5a3c5568ca3ebfb2366040d5

See more details on using hashes here.

File details

Details for the file countrystatecity_api-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for countrystatecity_api-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9b6e50727e0c5f7e7eebe7a59120324a328e34a6da4c3e780b3b2a6f844a6f67
MD5 d572a01735018ff49991919e520c3e57
BLAKE2b-256 eadb9f034fce1647728f4631253cf329fb7aa739ef5e7afdd45029d730221027

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

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