🎉 Introducing IPTrie — A fast, type-safe data structure for IP lookups with longest-prefix matching. Supports both IPv4 and IPv6. Learn more →
This is a Python client and command line interface (CLI) for the ipdata.co IP Geolocation API. ipdata offers a fast, highly-available API to enrich IP Addresses with Location, Company, Threat Intelligence and numerous other data attributes.
Note that you need an API Key to use this package. You can get a free one with a 1,500 daily request limit by Signing up here.
Visit our Documentation for more examples and tutorials.
Install the latest version of the cli with pip.
pip install ipdataTo use the IPTrie data structure, install with the trie extra (requires a C compiler):
pip install ipdata[trie]You need a valid API key from ipdata to use the library. You can get a free key by Signing up here.
Replace test with your API Key in the following examples.
You can look up the calling IP address, that is, the IP address of the computer you are running this on by not passing an IP address to the lookup method.
>>> import ipdata
>>> ipdata.api_key = "<YOUR API KEY>"
>>> ipdata.lookup()You can look up any valid IPv4 or IPv6 address by passing it to the lookup method.
>>> import ipdata
>>> ipdata.api_key = "<YOUR API KEY>"
>>> response = ipdata.lookup('69.78.70.144')
pprint(response)Sample Response
{
"ip": "69.78.70.144",
"is_eu": false,
"city": null,
"region": null,
"region_code": null,
"country_name": "United States",
"country_code": "US",
"continent_name": "North America",
"continent_code": "NA",
"latitude": 37.750999450683594,
"longitude": -97.8219985961914,
"postal": null,
"calling_code": "1",
"flag": "https://ipdata.co/flags/us.png",
"emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
"emoji_unicode": "U+1F1FA U+1F1F8",
"asn": {
"asn": "AS6167",
"name": "Verizon Business",
"domain": "verizon.com",
"route": "69.78.0.0/17",
"type": "business"
},
"company": {
"name": "Verizon Business",
"domain": "verizon.com",
"network": "69.78.0.0/17",
"type": "business"
},
"carrier": {
"name": "Verizon",
"mcc": "310",
"mnc": "004"
},
"languages": [
{
"name": "English",
"native": "English",
"code": "en"
}
],
"currency": {
"name": "US Dollar",
"code": "USD",
"symbol": "$",
"native": "$",
"plural": "US dollars"
},
"time_zone": {
"name": null,
"abbr": null,
"offset": null,
"is_dst": null,
"current_time": null
},
"threat": {
"is_tor": false,
"is_icloud_relay": false,
"is_proxy": false,
"is_datacenter": false,
"is_anonymous": false,
"is_known_attacker": false,
"is_known_abuser": false,
"is_threat": false,
"is_bogon": false,
"blocklists": []
},
"count": "3895",
"status": 200
}If you only need a single data attribute about an IP address you can extract it by passing a select_field parameter to the lookup method.
>>> import ipdata
>>> ipdata.api_key = "<YOUR API KEY>"
>>> ipdata.lookup('8.8.8.8', select_field='asn')Response
{
"asn": {
"asn": "AS15169",
"name": "Google LLC",
"domain": "about.google",
"route": "8.8.8.0/24",
"type": "business"
},
"status": 200
}If instead you need to get multiple specific fields you can pass a list of valid field names in a fields parameter.
>>> import ipdata
>>> ipdata.api_key = "<YOUR API KEY>"
>>> ipdata.lookup('8.8.8.8',fields=['ip','asn','country_name'])Response
{
"ip": "8.8.8.8",
"asn": {
"asn": "AS15169",
"name": "Google LLC",
"domain": "about.google",
"route": "8.8.8.0/24",
"type": "business"
},
"country_name": "United States",
"status": 200
}The API provides a /bulk endpoint that allows you to look up upto 100 IP addresses at a time. This is convenient for quickly clearing your backlog.
NOTE: Alternatively it is much simpler to process bulk lookups using the ipdata cli's batch command. All you need is a csv file with a list of IP addresses and you can get your results as either a JSON file or a CSV file! See the CLI Bulk Lookup Documentation for details.
>>> import ipdata
>>> ipdata.api_key = "<YOUR API KEY>"
>>> ipdata.bulk(['8.8.8.8','1.1.1.1'])Sample Response
{
"responses": [
{
"ip": "8.8.8.8",
"is_eu": false,
"city": null,
"region": null,
"region_code": null,
"country_name": "United States",
"country_code": "US",
"continent_name": "North America",
"continent_code": "NA",
"latitude": 37.750999450683594,
"longitude": -97.8219985961914,
"postal": null,
"calling_code": "1",
"flag": "https://ipdata.co/flags/us.png",
"emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
"emoji_unicode": "U+1F1FA U+1F1F8",
"asn": {
"asn": "AS15169",
"name": "Google LLC",
"domain": "about.google",
"route": "8.8.8.0/24",
"type": "business"
},
"company": {
"name": "Google LLC",
"domain": "google.com",
"network": "8.8.8.0/24",
"type": "business"
},
"languages": [
{
"name": "English",
"native": "English",
"code": "en"
}
],
"currency": {
"name": "US Dollar",
"code": "USD",
"symbol": "$",
"native": "$",
"plural": "US dollars"
},
"time_zone": {
"name": null,
"abbr": null,
"offset": null,
"is_dst": null,
"current_time": null
},
"threat": {
"is_tor": false,
"is_icloud_relay": false,
"is_proxy": false,
"is_datacenter": false,
"is_anonymous": false,
"is_known_attacker": false,
"is_known_abuser": false,
"is_threat": false,
"is_bogon": false,
"blocklists": []
},
"count": "3931"
},
{
"ip": "1.1.1.1",
"is_eu": null,
"city": null,
"region": null,
"region_code": null,
"country_name": null,
"country_code": null,
"continent_name": null,
"continent_code": null,
"latitude": null,
"longitude": null,
"postal": null,
"calling_code": null,
"flag": null,
"emoji_flag": null,
"emoji_unicode": null,
"asn": {
"asn": "AS13335",
"name": "Cloudflare, Inc.",
"domain": "cloudflare.com",
"route": "1.1.1.0/24",
"type": "business"
},
"company": {
"name": "Cloudflare, Inc.",
"domain": "cloudflare.com",
"network": "1.1.1.0/24",
"type": "business"
},
"languages": null,
"currency": {
"name": null,
"code": null,
"symbol": null,
"native": null,
"plural": null
},
"time_zone": {
"name": null,
"abbr": null,
"offset": null,
"is_dst": null,
"current_time": null
},
"threat": {
"is_tor": false,
"is_icloud_relay": false,
"is_proxy": false,
"is_datacenter": false,
"is_anonymous": false,
"is_known_attacker": false,
"is_known_abuser": false,
"is_threat": false,
"is_bogon": false,
"blocklists": []
},
"count": "3931"
}
],
"status": 200
}
An IPData client keeps a persistent HTTPS connection pool for as long as it lives. Create one client per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new client pays a fresh TLS handshake. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to HTTP/1.1 keep-alive otherwise.
The client is built on httpx and is thread-safe: one instance can serve every thread of a process at once (the CLI's batch command shares a single client across its thread pool). Idle connections are kept open for 60 seconds and up to 20 connections are pooled. Call close() at shutdown, or use the client as a context manager, to release the pool. Pass http2=False to stay on HTTP/1.1 keep-alive, or transport= to inject your own httpx.BaseTransport (for example an httpx.MockTransport in tests).
from ipdata import IPData
with IPData("<YOUR API KEY>") as client: # one client, many lookups
for ip in ["8.8.8.8", "1.1.1.1"]:
print(client.lookup(ip).country_name)The module-level helpers ipdata.lookup() and ipdata.bulk() share one process-wide client, ipdata.default_client, which is created on first use and lives until the process exits. Connections must not be shared across os.fork() or multiprocessing boundaries: create your IPData instance (or make the first module-level call) in the child process, after forking.
Failed requests are retried up to retry_limit times (default 7). GET and POST requests that fail with a transport error (connection reset, timeout) or with HTTP 500, 502, 503 or 504 are retried with exponential backoff: 0.5, 1, 2, 4, 8, 16 and 32 seconds for the default retry_backoff_factor=1. A 429 response is retried only when it carries a Retry-After header (waiting at most 60 seconds); without one it is returned immediately, so an exhausted quota fails fast. The HTTPS_PROXY and NO_PROXY environment variables are honoured; a custom CA bundle is read from SSL_CERT_FILE (httpx does not read REQUESTS_CA_BUNDLE).
IPData CLI needs Python 3.9+. Python Windows installation program provides PIP so you can install IPData CLI the same way:
pip install ipdata
ipdata --help
Usage: ipdata [OPTIONS] COMMAND [ARGS]...
Welcome to the ipdata CLI
Options:
--api-key TEXT Your ipdata API Key. Get one for free from
https://ipdata.co/sign-up.html
--help Show this message and exit.
Commands:
lookup* Lookup resources by using the IPData class methods.
batch Batch command for doing fast bulk processing.
init Initialize the CLI by setting an API key.
usage Get today's usage.
validate Validates a geofeed file. You need a valid API key from ipdata to use the cli. You can get a free key by Signing up here.
ipdata init <API Key>
_ _ _
(_)_ __ __| | __ _| |_ __ _
| | '_ \ / _` |/ _` | __/ _` |
| | |_) | (_| | (_| | || (_| |
|_| .__/ \__,_|\__,_|\__\__,_|
|_|
✨ Successfully initialized. You can also pass the --api-key <API Key> parameter to any command to specify a different API Key.
Running the ipdata command without any parameters will look up the IP address of the computer you are running the command on. Alternatively you can explicitly look up your own IP address by running ipdata.
ipdataTo pretty print the result pass the -p flag
╭────────────────────────────────╮ ╭────────────╮ ╭─────────────────╮ ╭──────────╮ ╭─────────────╮ ╭───────────────╮ ╭──────────────╮ ╭────────────────╮ ╭────────────────╮ ╭────────╮ ╭──────────────╮
│ ip │ │ is_eu │ │ city │ │ region │ │ region_code │ │ country_name │ │ country_code │ │ continent_name │ │ continent_code │ │ postal │ │ calling_code │
│ 24.24.24.24 │ │ False │ │ Syracuse │ │ New York │ │ NY │ │ United States │ │ US │ │ North America │ │ NA │ │ 13261 │ │ 1 │
╰────────────────────────────────╯ ╰────────────╯ ╰─────────────────╯ ╰──────────╯ ╰─────────────╯ ╰───────────────╯ ╰──────────────╯ ╰────────────────╯ ╰────────────────╯ ╰────────╯ ╰──────────────╯
╭────────────────────────────────╮ ╭────────────╮ ╭─────────────────╮ ╭──────────╮
│ flag │ │ emoji_flag │ │ emoji_unicode │ │ count │
│ https://ipdata.co/flags/us.png │ │ 🇺🇸 │ │ U+1F1FA U+1F1F8 │ │ 4086 │
╰────────────────────────────────╯ ╰────────────╯ ╰─────────────────╯ ╰──────────╯
╭────────────────────────────────╮ ╭──────────────────╮ ╭─────────────────╮ ╭────────────────╮ ╭───────────────────────────────╮ ╭───────────────────────╮
│ asn │ │ company │ │ languages │ │ currency │ │ time_zone │ │ threat │
│ ├── asn │ │ ├── name │ │ └── │ │ ├── name │ │ ├── name │ │ ├── is_tor │
│ │ AS11351 │ │ │ Rr-Route │ │ ├── name │ │ │ US Dollar │ │ │ America/New_York │ │ │ False │
│ ├── name │ │ ├── domain │ │ │ English │ │ ├── code │ │ ├── abbr │ │ ├── is_icloud_relay │
│ │ Charter Communications Inc │ │ │ │ │ ├── native │ │ │ USD │ │ │ EDT │ │ │ False │
│ ├── domain │ │ ├── network │ │ │ English │ │ ├── symbol │ │ ├── offset │ │ ├── is_proxy │
│ │ spectrum.com │ │ │ 24.24.0.0/19 │ │ └── code │ │ │ $ │ │ │ -0400 │ │ │ False │
│ ├── route │ │ └── type │ │ en │ │ ├── native │ │ ├── is_dst │ │ ├── is_datacenter │
│ │ 24.24.0.0/18 │ │ business │ ╰─────────────────╯ │ │ $ │ │ │ True │ │ │ False │
│ └── type │ ╰──────────────────╯ │ └── plural │ │ └── current_time │ │ ├── is_anonymous │
│ business │ │ US dollars │ │ 2022-07-15T16:59:44-04:00 │ │ │ False │
╰────────────────────────────────╯ ╰────────────────╯ ╰───────────────────────────────╯ │ ├── is_known_attacker │
│ │ False │
│ ├── is_known_abuser │
│ │ False │
│ ├── is_threat │
│ │ False │
│ ├── is_bogon │
│ │ False │
│ └── blocklists │
│ [] │
╰───────────────────────╯
You can pass any valid IPv4 or IPv6 address to the ipdata command to look it up. In case an invalid value is passed you will get the error ERROR 'BLEH' does not appear to be an IPv4 or IPv6 address".
ipdata 8.8.8.8Use -c to copy the results to the clipboard!
ipdata 1.1.1.1 -f ip -f asn -c
📋️ Copied result to clipboard!
Use --fields to filter the responses
ipdata --fields city --fields country_name'or use -f
ipdata 1.1.1.1 -f ip -f asn{
"ip": "1.1.1.1",
"asn": {
"asn": "AS13335",
"name": "Cloudflare, Inc.",
"domain": "cloudflare.com",
"route": "1.1.1.0/24",
"type": "business"
},
"status": 200
}Perhaps the most useful command provided by the CLI is the ability to process a csv file with a list of IP addresses and write the results to file as either CSV or JSONL/NDJSON! It could be a list of tens of thousands to millions of IP addresses and it will all be processed and the results filtered to your liking!
When you use the JSON output format, the results are written to the output file you provide with one result per line. Each line being a valid and full JSON response object. If you only need a few fields eg. only the country name you can specify a field argument with the names of the fields you want, if you combine this with the CSV output format you will get very clean results with only the data you need!
This is the default output format, so you could omit the --format JSON
ipdata batch my_ip_backlog.csv --output geolocation_results.json --format JSON
ipdata batch my_ip_backlog.csv --output results.csv --format CSV --fields ip --fields country_code
Either --fields or --exclude is required to use CSV output.
# ip,country_code
107.175.75.83,US
35.155.95.229,US
13.0.0.164,US
209.248.120.14,US
142.0.202.238,US
...A list of all the fields returned by the API is maintained at Response Fields
The following fields were added to the API in November 2026. The library passes them through unchanged, and both top-level flags are accepted by the fields and select_field parameters of lookup() and bulk() and by the CLI's -f and -e options.
| Field | Description |
|---|---|
is_satellite |
true when the address belongs to a satellite internet provider (for example Starlink). Present only when true, omitted otherwise. |
is_anycast |
true when the address is an anycast address announced from several locations at once (for example 8.8.8.8). Present only when true, omitted otherwise. |
threat.blocklists[].first_seen |
The UTC calendar date (a YYYY-MM-DD string) on which that blocklist first listed the address. Optional: some feeds, for example ipdata's own vpn feed, do not provide it. |
threat.blocklists[].last_seen |
The UTC calendar date (a YYYY-MM-DD string) on which that blocklist last listed the address. Optional, as above. |
threat.blocklists[].days_seen |
The number of days the address has been listed by that blocklist (an integer). Optional, as above. |
is_satellite and is_anycast are included in a response only when they are true. When absent they are missing from the dictionary and read as None through dot notation (response.is_satellite), so treat anything falsy as false. The dates are kept as strings and are not parsed. Fields the library does not know about yet are passed through as well.
Geofeed publishers can use the ipdata validate command to validate their geofeeds before submission to ipdata. This will catch most but not all issues that might cause processing your geofeed to fail.
The validation closely follows https://datatracker.ietf.org/doc/html/draft-google-self-published-geofeeds-02 with one caveat, we expect the country field to be provided at the minimum.
You can provide either a url or a path to a local file.
ipdata validate https://example.com/geofeed.txtor
ipdata validate geofeed.txtIPTrie is a production-ready, type-safe trie for IP addresses and CIDR prefixes with longest-prefix matching.
- Dual-stack support: Handles both IPv4 and IPv6 addresses seamlessly
- Longest-prefix matching: Automatically finds the most specific matching prefix
- Type-safe: Full generic type support with comprehensive type hints
- Pythonic API: Familiar dictionary-like interface (
[],in,del,len, iteration) - Input validation: Robust validation using Python's
ipaddressmodule - Custom exceptions: Clear, specific exceptions for better error handling
- Well-tested: Comprehensive test suite with edge cases covered
Note: IPTrie requires the
trieextra:pip install ipdata[trie]
from ipdata import IPTrie
# Create an IPTrie with string values
ip_trie: IPTrie[str] = IPTrie()
# Add network prefixes
ip_trie["10.0.0.0/8"] = "class-a-private"
ip_trie["10.1.0.0/16"] = "datacenter"
ip_trie["10.1.1.0/24"] = "web-servers"
# Longest-prefix matching
print(ip_trie["10.1.1.100"]) # "web-servers"
print(ip_trie["10.1.2.100"]) # "datacenter"
print(ip_trie["10.2.0.1"]) # "class-a-private"
# Check membership
print("10.1.1.50" in ip_trie) # True
print("192.168.1.1" in ip_trie) # False
# Get the matching prefix
print(ip_trie.parent("10.1.1.100")) # "10.1.1.0/24"
# Safe access with default
print(ip_trie.get("8.8.8.8", "unknown")) # "unknown"ip_trie: IPTrie[str] = IPTrie()
ip_trie["2001:db8::/32"] = "documentation"
ip_trie["2001:db8:1::/48"] = "specific-block"
print(ip_trie["2001:db8:1::1"]) # "specific-block"
print(ip_trie["2001:db8:2::1"]) # "documentation"from ipdata import IPTrie
classifier: IPTrie[dict] = IPTrie()
classifier["10.0.0.0/8"] = {"type": "private", "rfc": "1918"}
classifier["172.16.0.0/12"] = {"type": "private", "rfc": "1918"}
classifier["192.168.0.0/16"] = {"type": "private", "rfc": "1918"}
classifier["0.0.0.0/0"] = {"type": "public", "rfc": None}
def classify_ip(ip: str) -> dict:
return classifier.get(ip, {"type": "unknown"})
print(classify_ip("192.168.1.100")) # {"type": "private", "rfc": "1918"}
print(classify_ip("8.8.8.8")) # {"type": "public", "rfc": None}from ipdata import IPTrie
geo_db: IPTrie[str] = IPTrie()
geo_db["8.8.8.0/24"] = "US"
geo_db["1.1.1.0/24"] = "AU"
def get_country(ip: str) -> str:
return geo_db.get(ip, "Unknown")from ipdata import IPTrie
acl: IPTrie[bool] = IPTrie()
acl["192.168.1.0/24"] = True # Allow internal
acl["10.0.0.0/8"] = True # Allow VPN
acl["0.0.0.0/0"] = False # Deny all others
def is_allowed(ip: str) -> bool:
return acl.get(ip, False)IPTrie[T]() # Create an empty IPTrie with value type T| Method | Description |
|---|---|
__setitem__(key, value) |
Set value for IP/prefix |
__getitem__(key) |
Get value using longest-prefix match (raises KeyNotFoundError) |
get(key, default=None) |
Get value or default if not found |
__delitem__(key) |
Delete exact prefix |
__contains__(key) |
Check if IP matches any prefix |
has_key(key) |
Check if exact prefix exists |
parent(key) |
Get the longest matching prefix string |
children(key) |
Get all more specific prefixes |
__len__() |
Count of all prefixes |
__iter__() |
Iterate over all prefixes |
keys() |
Iterator over prefixes |
values() |
Iterator over values |
items() |
Iterator over (prefix, value) tuples |
clear() |
Remove all entries |
| Exception | Description |
|---|---|
IPTrieError |
Base exception class |
InvalidIPError |
Invalid IP address or network format |
KeyNotFoundError |
No matching prefix found (also a KeyError) |
IPTrie validates all inputs using Python's ipaddress module:
from ipdata import IPTrie, InvalidIPError
ip_trie: IPTrie[str] = IPTrie()
# These work
ip_trie["192.168.1.0/24"] = "valid"
ip_trie["2001:db8::1"] = "valid"
# These raise InvalidIPError
try:
ip_trie["not-an-ip"] = "invalid"
except InvalidIPError as e:
print(f"Error: {e}")
try:
ip_trie[""] = "empty"
except InvalidIPError as e:
print(f"Error: {e}")IPTrie uses Patricia tries (via pytricia) internally, providing:
- O(k) lookup time where k is the prefix length (32 for IPv4, 128 for IPv6)
- Memory efficient storage of overlapping prefixes
- Fast iteration over all prefixes
A list of possible errors is available at Status Codes
Install test dependencies:
pip install ipdata[test,trie]To run all tests
pytestThe tests marked api call the live API and need an IPDATA_API_KEY environment variable (or a src/ipdata/.env file). To run only the offline tests, as CI does for pull requests:
pytest -m "not api"Releases are cut by CI from the version in setup.cfg:
- Bump
versioninsetup.cfgin a pull request and merge it intomaster. - CI runs the tests, tags the merge commit
x.y.z(the version, with novprefix), creates the GitHub release with generated notes and publishes the package to PyPI.
A merge whose version is already tagged publishes nothing. Pushing a tag by hand still builds and publishes that tag as before.