Get a visitor's country from an IP address in Python
A few lines of standard-library Python call the ipatlas API and return the country, with a timeout, a small cache and sane failure handling.
1. One lookup
No key and no install needed:
import json
import urllib.request
req = urllib.request.Request("https://api.ipatlas.dev/v1/8.8.8.8", headers={"User-Agent": "my-app/1.0"})
with urllib.request.urlopen(req, timeout=2) as resp:
print(json.load(resp)["country_code"]) # US
The response also has country, region, city, timezone and asn; see the docs. Pass your own User-Agent: the default Python-urllib one is rejected with HTTP 403. The requests package sends its own and works as is.
2. A reusable helper with a cache
For a web app, validate the address, skip private ranges, cache the answer and never let the lookup slow you down. Optionally create a free key on the home page and export it as IPATLAS_KEY. Save this as geo.py:
import ipaddress
import json
import os
import time
import urllib.error
import urllib.request
# Set your own User-Agent: the default "Python-urllib" one is blocked (HTTP 403).
UA = "my-app/1.0"
KEY = os.environ.get("IPATLAS_KEY") # optional; keyless is limited to 1,000 requests/day/IP
TTL = 6 * 3600 # successful answers: 6 hours
MAX = 10_000 # bound the memory use
_cache = {} # ip -> (expires_at, result)
def country_of(ip):
"""Return {"code": "DE", "name": "Germany"}, or None if unknown or the lookup failed."""
try:
addr = ipaddress.ip_address((ip or "").strip())
except ValueError:
return None
if not addr.is_global: # private, loopback and reserved ranges have no location
return None
ip = str(addr)
hit = _cache.get(ip)
if hit and hit[0] > time.time():
return hit[1]
result, ttl = None, 60 # failures: remember for a minute
headers = {"User-Agent": UA}
if KEY:
headers["X-API-Key"] = KEY
req = urllib.request.Request(f"https://api.ipatlas.dev/v1/{ip}", headers=headers)
try:
with urllib.request.urlopen(req, timeout=2) as resp:
data = json.load(resp)
if data.get("country_code"):
result, ttl = {"code": data["country_code"], "name": data.get("country")}, TTL
except (urllib.error.URLError, TimeoutError, ValueError):
pass # includes HTTP 429: urllib raises HTTPError, a URLError
if len(_cache) >= MAX:
_cache.pop(next(iter(_cache))) # drop the oldest entry
_cache[ip] = (time.time() + ttl, result)
return result
if __name__ == "__main__":
print(country_of("8.8.8.8")) # {'code': 'US', 'name': 'United States'}
print(country_of("10.0.0.1")) # None (private address)
3. Use it in Flask
from flask import Flask, request
from werkzeug.middleware.proxy_fix import ProxyFix
from geo import country_of
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) # one proxy in front of you; remove this line if there is none
@app.get("/")
def home():
c = country_of(request.remote_addr) # the visitor's address, after ProxyFix
return f"Hello from {c['name'] if c else 'somewhere'}"
In Django, pass request.META.get("REMOTE_ADDR") to country_of. Only trust X-Forwarded-For when your own proxy sets it; a visitor can send any value in that header.
Notes
- The cache is per process and lost on restart. Caching keeps a busy site inside the 50,000 monthly lookups of the free key.
country_ofreturnsNoneon a timeout, a429or an unknown address, so decide on a default (for example your main language or currency).- Do not log full visitor addresses together with locations unless you need to; see our privacy page for what we store.
Limits: without a key, 1,000 requests per day per IP address (10 req/s); with a free key, 50,000 requests per month (50 req/s). Over a limit the API returns 429. IP geolocation is approximate: country is usually right, city often is not.
Attribution
The data is DB-IP Lite, licensed CC BY 4.0. If you show or use it in a public product, link to DB-IP, for example in your footer:
<a href="https://db-ip.com">IP Geolocation by DB-IP</a>
See the API docs for fields, limits and error codes.