Python SDK (taxql)
The official Python client for the TaxQL API. Hand-written, sync +
async via httpx, typed responses via Pydantic v2, automatic
retries on 429 and 5xx with exponential backoff (honoring
Retry-After headers).
Installation
Section titled “Installation”pip install taxqlPyPI page: pypi.org/project/taxql (current version 0.2.1).
The package depends only on httpx >= 0.24 and pydantic >= 2.0.
Quickstart
Section titled “Quickstart”from taxql import TaxQL
client = TaxQL(api_key="your-key-here")
response = client.lookup( state="tx", address="500 W 5th St, Austin, TX 78701",)
print(response.combined_rate) # 0.0825print(response.confidence) # "exact"print(response.rate.county) # "TRAVIS"Client reference
Section titled “Client reference”TaxQL(api_key, *, base_url, timeout, max_retries, http_client)
Section titled “TaxQL(api_key, *, base_url, timeout, max_retries, http_client)”| Arg | Type | Default | Notes |
|---|---|---|---|
api_key | str | required | Raises ValueError if empty. |
base_url | str | "https://api.taxql.com" | Trailing slash stripped. |
timeout | float | 10.0 | Per-request, in seconds. |
max_retries | int | 3 | Retried on 429 + 5xx + timeout. |
http_client | httpx.Client | None | None | Inject your own — SDK won’t close it. |
client.lookup(state, *, ...) -> TaxResponse
Section titled “client.lookup(state, *, ...) -> TaxResponse”Exactly one input mode per call:
client.lookup(state="tx", address="...", city="...", zip="...")client.lookup(state="wa", zip="98039")client.lookup(state="wa", location="Seattle")client.lookup(state="ca", lat=34.05, lng=-118.25)Optional:
as_of: date— historical queryperiod: str— explicit YYYYQ period code (advanced)
client.health() -> HealthResponse
Section titled “client.health() -> HealthResponse”Hits GET /healthz. Returns a HealthResponse with a status string
when the API is alive.
Context manager support
Section titled “Context manager support”with TaxQL(api_key="...") as client: client.lookup(state="tx", zip="75034")# httpx.Client closed automatically.AsyncTaxQL — async equivalent
Section titled “AsyncTaxQL — async equivalent”Same constructor surface, every I/O method is async:
import asynciofrom taxql import AsyncTaxQL
async def main(): async with AsyncTaxQL(api_key="...") as client: return await asyncio.gather( client.lookup(state="tx", zip="75034"), client.lookup(state="wa", zip="98039"), client.lookup(state="ca", zip="94110"), )
asyncio.run(main())Reading the response
Section titled “Reading the response”A lookup returns the single applicable rate — the convenience
properties give you the common answers, and response.rate /
response.meta hold the rest:
response = client.lookup(state="tx", address="500 W 5th St, Austin, TX 78701")
response.combined_rate # float accessor — total rate, e.g. 0.0825response.combined_rate_string # "0.08250" — the lossless wire stringresponse.confidence # "exact" | "high" | "medium" | "low" | "none"response.warnings # list of advisories (see below)
response.rate.state # "TX"response.rate.county # "TRAVIS"response.rate.state_rate # "0.06250" — wire STRING (since 0.2.0)response.rate.state_rate_as_float() # 0.0625 — explicit accessorresponse.rate.combined_district_rate # "0.02000" — sum of special-district ratesresponse.meta.resolved_via # "address_locator"response.meta.effective_date # "2025-03-01" or None — the served rate's # legal-effect date; None unless the source # publishes a genuine per-row effective dateresponse.meta.effective_date_basis # "publisher_effective_date" | "edition_only" # | "no_published_date" (provenance)response.meta.period # DEPRECATED: quarter code OR ISO date OR None; # prefer effective_date + effective_date_basisSince 0.2.0, rate fields are preserved as the API’s fixed 5-decimal
strings ("0.06250") — never coerced to float — so binary floating point
never touches tax arithmetic. Convert explicitly with rate_to_float(...), the
Rate.*_as_float() accessors, or response.combined_rate (a float accessor);
for exact money math read the string and use decimal.Decimal. This matches the
PHP and Node SDKs. For the verbose per-jurisdiction body (every
candidate row, component breakdown, resolved place), request
mode=full on the raw HTTP API and
read it via response.model_dump().
Warnings
Section titled “Warnings”response.warnings is a list whose items are either plain strings or
structured objects carrying a code:
for w in response.warnings: if isinstance(w, dict) and w.get("code") == "place_input_mismatch": print(f"input '{w['customer_input_place']}' resolved to " f"'{w['resolved_place_name']}'") elif isinstance(w, str): print(w)Error handling
Section titled “Error handling”from taxql import ( TaxQLError, # Base — catch this for "any SDK error" AuthError, # 401 — invalid/missing API key PaymentRequiredError,# 402 — billing/subscription issue ForbiddenError, # 403 — your plan doesn't include this RateLimitError, # 429 — has .retry_after attribute NotFoundError, # 404 — address/ZIP couldn't be resolved ValidationError, # 400/422 — request invalid ServiceError, # 5xx — upstream issue)
try: response = client.lookup(state="tx", address="...") rate = response.combined_rateexcept AuthError: print("Bad API key — check the dashboard")except RateLimitError as exc: print(f"Rate limited; SDK already retried — give up after {exc.retry_after}s")except NotFoundError: print("Address could not be resolved — try ZIP fallback")except ServiceError: print("Upstream is down — retry later")except TaxQLError as exc: print(f"Unexpected SDK error: {exc} (status={exc.status_code})")Every exception carries status_code, the machine error_code, the
support_reference (quote it to support), and the parsed
response_body:
except TaxQLError as exc: print(exc.error_code, exc.support_reference, exc.status_code)Retries
Section titled “Retries”429 and 5xx responses are retried automatically (default
max_retries=3) with exponential backoff. If the response
includes a Retry-After header, the SDK honors it; otherwise the
delay is min(2 ** attempt * 0.5, 30) seconds — 0.5s, 1s, 2s, 4s,
8s, then capped at 30.
4xx errors other than 429 raise immediately. There’s no point
retrying a malformed request.
Models reference
Section titled “Models reference”All response models live in taxql.models and are re-exported from
the top level:
TaxResponse— top-level lookup response:rate: Rate,meta: Meta, plus convenience propertiescombined_rate/confidence/warnings.Rate— the resolved rate:state,zip,city,county,country,country_rate(national-level rate;0for the US),state_rate,county_rate,city_rate,combined_district_rate,combined_rate,freight_taxable.Meta— diagnostics:resolved_via,confidence,period,warnings.HealthResponse—status.
All models use extra="allow", so newer API fields don’t break older
clients. Reach anything not surfaced as a typed attribute via
response.model_dump().
Async concurrency pattern
Section titled “Async concurrency pattern”import asynciofrom taxql import AsyncTaxQL
async def lookup_many(api_key: str, queries: list[dict]): async with AsyncTaxQL(api_key=api_key) as client: results = await asyncio.gather( *(client.lookup(**q) for q in queries), return_exceptions=True, ) return results
# Use:queries = [ {"state": "tx", "address": "500 W 5th St, Austin, TX 78701"}, {"state": "wa", "zip": "98039"}, {"state": "ca", "lat": 34.05, "lng": -118.25},]results = asyncio.run(lookup_many("your-key", queries))return_exceptions=True keeps one failed lookup from cancelling the
others. Walk results and branch on isinstance(r, Exception) for
each entry.
Source
Section titled “Source”The package source lives at
github.com/taxql/taxql-python
(repo pending — currently in-repo at services/sdk/python/).
Changelog
Section titled “Changelog”See changelog.