IP geolocation in Python is straightforward when you use the right API. This guide shows you how to look up IP addresses using ipinfo.im — a free IP geolocation API that requires no API key and no registration. We’ll start with a basic request, then cover error handling, batch processing, and production-ready patterns.
Prerequisites
- Python 3.7 or later
- The
requestslibrary (pip install requests)
That’s it. No API key to generate, no account to create.
Basic IP Lookup
The simplest possible IP geolocation in Python is just a few lines:
import requests
def get_ip_info(ip: str) -> dict:
"""Look up geolocation data for an IP address."""
url = f"https://ipinfo.im/api/?ip={ip}"
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
# Usage
data = get_ip_info("8.8.8.8")
print(f"IP: {data['ip']}")
print(f"Country: {data['country']} ({data['country_name']})")
print(f"City: {data['city']}, {data['region']}")
print(f"ISP: {data['org']}")
print(f"Timezone: {data['timezone']}")
Output:
IP: 8.8.8.8
Country: US (United States)
City: Mountain View, California
ISP: AS15169 Google LLC
Timezone: America/Los_Angeles
Look Up Your Own IP
To get the geolocation of the machine running your script:
import requests
def get_my_ip_info() -> dict:
"""Get geolocation data for the current machine's public IP."""
response = requests.get("https://ipinfo.im/api/", timeout=10)
response.raise_for_status()
return response.json()
data = get_my_ip_info()
print(f"My IP: {data['ip']}")
print(f"My location: {data['city']}, {data['country']}")
Production-Ready Lookup with Error Handling
In production code, you should handle network errors, timeouts, and unexpected API responses gracefully:
import requests
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class IPInfoClient:
"""Client for the ipinfo.im IP geolocation API."""
BASE_URL = "https://ipinfo.im/api/"
def __init__(self, timeout: int = 10):
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "MyApp/1.0 (ip-lookup)",
"Accept": "application/json",
})
def lookup(self, ip: str) -> Optional[dict]:
"""
Look up geolocation data for an IP address.
Args:
ip: IPv4 or IPv6 address to look up.
Returns:
dict with IP data, or None if the lookup failed.
"""
try:
url = self.BASE_URL if not ip else f"{self.BASE_URL}?ip={ip}"
response = self.session.get(url, timeout=self.timeout)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
logger.warning(f"Timeout looking up IP {ip}")
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error looking up IP {ip}: {e}")
except requests.exceptions.ConnectionError:
logger.error("Could not connect to ipinfo.im API")
except ValueError:
logger.error(f"Invalid JSON response for IP {ip}")
return None
def get_country(self, ip: str) -> Optional[str]:
"""Return the ISO country code for an IP, or None."""
data = self.lookup(ip)
return data.get("country") if data else None
def get_city(self, ip: str) -> Optional[str]:
"""Return the city name for an IP, or None."""
data = self.lookup(ip)
return data.get("city") if data else None
# Usage
client = IPInfoClient()
result = client.lookup("1.1.1.1")
if result:
print(f"Country: {result['country']}")
print(f"ISP: {result['org']}")
else:
print("Lookup failed")
Batch Querying Multiple IPs
When you need to look up a list of IPs, process them sequentially with short delays to be respectful of the free API:
import requests
import time
from typing import List, Dict, Optional
def batch_lookup(
ips: List[str],
delay_seconds: float = 0.2,
) -> Dict[str, Optional[dict]]:
"""
Look up a list of IP addresses.
Args:
ips: List of IP address strings.
delay_seconds: Pause between requests (be respectful of the free API).
Returns:
Dict mapping each IP to its lookup result (or None on failure).
"""
results = {}
session = requests.Session()
for ip in ips:
try:
url = f"https://ipinfo.im/api/?ip={ip}"
response = session.get(url, timeout=10)
response.raise_for_status()
results[ip] = response.json()
except Exception as e:
print(f"Failed to look up {ip}: {e}")
results[ip] = None
if delay_seconds > 0:
time.sleep(delay_seconds)
return results
# Example: look up a list of IPs
ips_to_check = [
"8.8.8.8", # Google DNS
"1.1.1.1", # Cloudflare DNS
"9.9.9.9", # Quad9 DNS
"208.67.222.222", # OpenDNS
]
results = batch_lookup(ips_to_check)
for ip, data in results.items():
if data:
print(f"{ip:20s} -> {data['country']:4s} | {data['city']:20s} | {data['org']}")
else:
print(f"{ip:20s} -> lookup failed")
Output:
8.8.8.8 -> US | Mountain View | AS15169 Google LLC
1.1.1.1 -> AU | Research | AS13335 Cloudflare, Inc.
9.9.9.9 -> US | Berkeley | AS19281 Quad9
208.67.222.222 -> US | San Jose | AS36692 Cisco OpenDNS, LLC
Caching Results with functools.lru_cache
IP geolocation data changes rarely. Cache results to reduce API calls and improve performance:
import requests
import functools
@functools.lru_cache(maxsize=1000)
def lookup_ip_cached(ip: str) -> tuple:
"""
Cached IP lookup. Returns a tuple for hashability.
Cache holds up to 1000 unique IPs.
"""
try:
response = requests.get(
f"https://ipinfo.im/api/?ip={ip}",
timeout=10
)
response.raise_for_status()
data = response.json()
return (
data.get("country", ""),
data.get("city", ""),
data.get("org", ""),
data.get("timezone", ""),
)
except Exception:
return ("", "", "", "")
# First call hits the API
country, city, org, tz = lookup_ip_cached("8.8.8.8")
print(f"Country: {country}, City: {city}")
# Second call is served from cache instantly
country, city, org, tz = lookup_ip_cached("8.8.8.8")
print(f"Cache hit! Country: {country}")
Real-World Example: Geo-Based Access Control
Here’s a practical example — a Flask middleware that logs requests with geographic context:
from flask import Flask, request, g
import requests
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
def get_ip_country(ip: str) -> str:
"""Return ISO country code for an IP, or 'XX' on failure."""
try:
resp = requests.get(f"https://ipinfo.im/api/?ip={ip}", timeout=5)
return resp.json().get("country", "XX")
except Exception:
return "XX"
@app.before_request
def enrich_request_with_geo():
"""Add country code to the request context."""
client_ip = request.headers.get("X-Forwarded-For", request.remote_addr)
# Take first IP if X-Forwarded-For contains multiple
client_ip = client_ip.split(",")[0].strip()
g.country = get_ip_country(client_ip)
@app.route("/api/data")
def data_endpoint():
# Block requests from certain regions (example only)
blocked_countries = {"CN", "RU"} # adjust to your use case
if g.country in blocked_countries:
return {"error": "Service not available in your region"}, 403
logger.info(f"Request from country: {g.country}")
return {"message": "Hello!", "your_country": g.country}
if __name__ == "__main__":
app.run(debug=True)
Summary
The ipinfo.im API makes IP geolocation in Python simple:
- Install
requests— that’s the only dependency - Call
https://ipinfo.im/api/?ip=<ip>and parse the JSON - Add error handling with try/except for production use
- Cache results to reduce redundant API calls
- Add delays between batch requests to be a good API citizen
No API key, no subscription, no hassle. Try it now at ipinfo.im.