Prerequisites
You need three things:
- Python 3.8 or newer.
- The
requestslibrary. - A free API Ninjas API key — issued instantly, no credit card. If you already have an account, it's on your profile page.
pip install requestsStep 1: Look up your first IP
The endpoint is /v1/iplookup and takes one parameter: address, an IPv4 or IPv6 address.
import requests
response = requests.get(
'https://api.api-ninjas.com/v1/iplookup',
params={'address': '24.48.0.1'},
headers={'X-Api-Key': 'YOUR_API_KEY'},
timeout=10,
)
response.raise_for_status()
info = response.json()
print(f"{info['city']}, {info['country']}")
# Montreal, CanadaThe request on the wire and the full response:
{
"address": "24.48.0.1",
"is_valid": true,
"country": "Canada",
"country_code": "CA",
"region": "Quebec",
"city": "Montreal",
"zip": "H1S",
"lat": 45.5808,
"lon": -73.5825,
"timezone": "America/Toronto",
"isp": "Le Groupe Videotron Ltee"
}Step 2: Read the response
Everything arrives in one flat object: the location chain (country, region, city, zip), coordinates (lat, lon), the IANA timezone, and the isp name. Two reading tips: is_valid confirms the address parsed correctly, and city-level placement is an estimate — IP geolocation reliably answers “which country and region,” not “which street.”
Step 3: Harden the client
Move the key into an environment variable and share one Session across lookups — the connection reuse matters as soon as you process more than a handful of addresses:
import os
import requests
API_URL = 'https://api.api-ninjas.com/v1/iplookup'
session = requests.Session()
session.headers['X-Api-Key'] = os.environ.get('API_NINJAS_KEY', 'YOUR_API_KEY')
def lookup_ip(ip: str) -> dict:
"""Geolocate one IPv4 or IPv6 address."""
response = session.get(API_URL, params={'address': ip}, timeout=10)
response.raise_for_status()
return response.json()
info = lookup_ip('24.48.0.1')
print(f"{info['city']}, {info['region']}, {info['country']} ({info['isp']})")Run export API_NINJAS_KEY=your_key once, and lookup_ip() becomes the building block for the batch job in the next step.
Step 4: Geolocate a whole log file
The realistic job isn't one IP — it's an access log with thousands of lines and heavy repetition. Two habits keep that cheap: deduplicate before looking anything up, and cache results so tomorrow's run only pays for new addresses:
import re
import sqlite3
LOG_LINE = re.compile(r'^(\S+)\s')
# 1. Collect unique IPs from the access log
ips = set()
with open('access.log') as f:
for line in f:
match = LOG_LINE.match(line)
if match:
ips.add(match.group(1))
# 2. Cache lookups so re-runs never repeat a request
db = sqlite3.connect('ip_cache.db')
db.execute('CREATE TABLE IF NOT EXISTS ip_cache (ip TEXT PRIMARY KEY, country TEXT, city TEXT)')
for ip in ips:
cached = db.execute('SELECT country, city FROM ip_cache WHERE ip = ?', (ip,)).fetchone()
if cached is None:
info = lookup_ip(ip)
cached = (info['country'], info['city'])
db.execute('INSERT INTO ip_cache VALUES (?, ?, ?)', (ip, *cached))
db.commit()
print(f'{ip:15} {cached[0]:20} {cached[1]}')The SQLite cache is the difference between thousands of requests per run and a few dozen — most log traffic comes from IPs you've already seen.
Step 5: Handle errors
Malformed input is the common failure here — log files contain garbage. A 400 means the address didn't parse; handle it per-IP so one bad line doesn't kill the batch:
from requests.exceptions import HTTPError, RequestException
try:
info = lookup_ip('not-an-ip')
except HTTPError as error:
status = error.response.status_code
if status == 400:
print('Invalid IP address format')
elif status == 401:
print('Missing or invalid API key')
elif status == 429:
print('Rate limit exceeded — slow down and retry')
else:
print(f'HTTP {status}: {error.response.text}')
except RequestException as error:
print(f'Network error: {error}')Where to go from here
Once you have coordinates for a visitor, the rest of the location stack is one request away: the Timezone API for local time, the Weather API for conditions, and the Geocoding API for the reverse direction — place names to coordinates. The complete script is below.