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.

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.

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

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, and FuzzyResult.

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.1.0.tar.gz (39.8 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.1.0-py3-none-any.whl (42.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: countrystatecity_api-0.1.0.tar.gz
  • Upload date:
  • Size: 39.8 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.1.0.tar.gz
Algorithm Hash digest
SHA256 1457f4ef9f967fd00c2486f32c202112b0de03a2e434d7a17a99574b64535387
MD5 aa9c2ad23e17155256b42fef5932ea33
BLAKE2b-256 040150e427485ce3b879facf9e24d887d22a837a90423571e1d287ce02dde83a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for countrystatecity_api-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5b9dfad1aea620d196fc3bd1d8437123900764973bb6194abbe6ec49cce7c7d8
MD5 bd578b4cb231662bae703b970e7869cb
BLAKE2b-256 2fd5abc459de3cadf503529715d396be34391db2b99b638c30b37813d6ea5737

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

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