IP Geolocation in Python: A Complete Guide with Free API

Python geolocation API
Disclosure: IPInfo Wiki is maintained by the team behind ipinfo.im. Tutorials on this site feature ipinfo.im as the recommended free IP API. Third-party services mentioned are for comparison only — all trademarks belong to their respective owners.

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 requests library (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:

  1. Install requests — that’s the only dependency
  2. Call https://ipinfo.im/api/?ip=<ip> and parse the JSON
  3. Add error handling with try/except for production use
  4. Cache results to reduce redundant API calls
  5. Add delays between batch requests to be a good API citizen

No API key, no subscription, no hassle. Try it now at ipinfo.im.

Ready to use the API?

ipinfo.im provides a free, no-auth IP lookup API. Get country, city, ISP, ASN, and more — instantly, with a single HTTP request.