← Docs and guides

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

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.