Getting the public IP in Python — scripts, Django, FastAPI
Whether you're writing a quick script, building a Django web app, or designing a FastAPI microservice, knowing the public IP of your server or client is a recurring need. This article covers all three scenarios with practical code, using IPPubblico.org — a free API with no key required, HTTPS, and CORS enabled.
Why IPPubblico?
Before diving into code, here's a quick summary of why it's worth using:
- No API key — works immediately, zero setup
- HTTPS only — secure by default
- Plain text endpoint — no parsing for simple cases
- Full JSON endpoint — city, country, ISP, ASN, timezone when needed
- No hard rate limit — soft limiting applies for abuse prevention
- Commercial use allowed on the free tier
Use case 1 — Simple script with requests
The most common scenario: a standalone script that needs to know its own public IP.
import requests
def get_public_ip():
try:
response = requests.get(
'https://ipv4.ippubblico.org/',
timeout=5
)
response.raise_for_status()
return response.text.strip()
except requests.RequestException as e:
print(f"Failed to get IP: {e}")
return None
if __name__ == '__main__':
ip = get_public_ip()
print(f"Public IP: {ip}")
# Public IP: 203.0.113.42
Use case 2 — Full geolocation with requests
When you need country, city, ISP and timezone in addition to the IP:
import requests
from dataclasses import dataclass
from typing import Optional
@dataclass
class IPInfo:
ip: str
country: Optional[str]
country_code: Optional[str]
city: Optional[str]
region: Optional[str]
isp: Optional[str]
timezone: Optional[str]
lat: Optional[float]
lon: Optional[float]
def get_ip_info(ip: str = None) -> Optional[IPInfo]:
"""
Get geolocation info for an IP address.
If ip is None, returns info for the current public IP.
"""
url = 'https://ippubblico.org/?api=1'
if ip:
url += f'&ip={ip}'
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
if data.get('status') != 'ok':
return None
geo = data.get('geo', {})
return IPInfo(
ip=data.get('ip', ''),
country=geo.get('country'),
country_code=geo.get('country_code'),
city=geo.get('city'),
region=geo.get('region'),
isp=data.get('isp'),
timezone=data.get('timezone'),
lat=geo.get('lat'),
lon=geo.get('lon'),
)
except requests.RequestException as e:
print(f"Request failed: {e}")
return None
if __name__ == '__main__':
info = get_ip_info()
if info:
print(f"IP: {info.ip}")
print(f"Country: {info.country} ({info.country_code})")
print(f"City: {info.city}, {info.region}")
print(f"ISP: {info.isp}")
print(f"Timezone: {info.timezone}")
Use case 3 — Async script with httpx
For async Python scripts using httpx:
import asyncio
import httpx
async def get_public_ip() -> str | None:
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ipv4.ippubblico.org/')
response.raise_for_status()
return response.text.strip()
except httpx.HTTPError as e:
print(f"Failed to get IP: {e}")
return None
async def get_ip_info() -> dict | None:
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
response.raise_for_status()
data = response.json()
return data if data.get('status') == 'ok' else None
except httpx.HTTPError as e:
print(f"Failed to get IP info: {e}")
return None
async def main():
ip = await get_public_ip()
print(f"Public IP: {ip}")
info = await get_ip_info()
if info:
print(f"Country: {info['geo']['country']}")
print(f"City: {info['geo']['city']}")
asyncio.run(main())
Use case 4 — Django middleware for country detection
A Django middleware that detects the user's country on every request and adds it to request.country_code. Useful for region-based content, currency selection, or logging.
# myapp/middleware.py
import requests
from django.core.cache import cache
CACHE_TIMEOUT = 3600 # 1 hour per IP
class CountryDetectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
client_ip = self._get_client_ip(request)
request.client_ip = client_ip
request.country_code = self._get_country(client_ip)
response = self.get_response(request)
return response
def _get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
def _get_country(self, ip: str) -> str | None:
if not ip:
return None
cache_key = f'country_{ip}'
cached = cache.get(cache_key)
if cached:
return cached
try:
response = requests.get(
'https://ippubblico.org/?api=1',
timeout=3
)
data = response.json()
country_code = data.get('geo', {}).get('country_code')
if country_code:
cache.set(cache_key, country_code, CACHE_TIMEOUT)
return country_code
except Exception:
return None
Register it in settings.py:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'myapp.middleware.CountryDetectionMiddleware', # add here
# ... rest of middleware
]
Use it in any view:
# views.py
from django.http import JsonResponse
def my_view(request):
return JsonResponse({
'ip': request.client_ip,
'country': request.country_code,
})
Use case 5 — Django view with per-request geolocation
For cases where you need full geolocation data in a specific view, not globally:
# views.py
import requests
from django.http import JsonResponse
from django.views import View
class UserLocationView(View):
def get(self, request):
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() \
or request.META.get('REMOTE_ADDR')
try:
response = requests.get(
'https://ippubblico.org/?api=1',
timeout=5
)
data = response.json()
geo = data.get('geo', {})
return JsonResponse({
'ip': data.get('ip'),
'country': geo.get('country'),
'country_code': geo.get('country_code'),
'city': geo.get('city'),
'timezone': data.get('timezone'),
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
Use case 6 — FastAPI dependency injection
The cleanest FastAPI pattern: a reusable dependency that injects IP info into any route.
# dependencies.py
import httpx
from functools import lru_cache
from fastapi import Request, Depends
from typing import Optional
from pydantic import BaseModel
class GeoInfo(BaseModel):
city: Optional[str] = None
region: Optional[str] = None
country: Optional[str] = None
country_code: Optional[str] = None
lat: Optional[float] = None
lon: Optional[float] = None
class IPInfo(BaseModel):
ip: str
isp: Optional[str] = None
timezone: Optional[str] = None
geo: GeoInfo = GeoInfo()
def get_client_ip(request: Request) -> str:
forwarded = request.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0].strip()
return request.client.host
async def get_ip_info(request: Request) -> Optional[IPInfo]:
ip = get_client_ip(request)
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
data = response.json()
if data.get('status') == 'ok':
return IPInfo(**{
'ip': data.get('ip', ip),
'isp': data.get('isp'),
'timezone': data.get('timezone'),
'geo': GeoInfo(**data.get('geo', {}))
})
except Exception:
pass
return None
Use in any route:
# main.py
from fastapi import FastAPI, Depends
from dependencies import IPInfo, get_ip_info
app = FastAPI()
@app.get('/location')
async def location(ip_info: IPInfo = Depends(get_ip_info)):
if not ip_info:
return {'error': 'Could not detect location'}
return {
'ip': ip_info.ip,
'country': ip_info.geo.country,
'city': ip_info.geo.city,
'timezone': ip_info.timezone,
}
Use case 7 — FastAPI with caching (Redis)
For high-traffic APIs, cache the geolocation result per IP to avoid repeated external calls:
# dependencies_cached.py
import httpx
import json
from fastapi import Request
from typing import Optional
import redis.asyncio as redis
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
CACHE_TTL = 3600 # 1 hour
async def get_ip_info_cached(request: Request) -> Optional[dict]:
forwarded = request.headers.get('X-Forwarded-For')
ip = forwarded.split(',')[0].strip() if forwarded else request.client.host
cache_key = f'ipinfo:{ip}'
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
data = response.json()
if data.get('status') == 'ok':
await redis_client.setex(cache_key, CACHE_TTL, json.dumps(data))
return data
except Exception:
pass
return None
Handling rate limits
If your application makes many requests from the same IP in a short period, you may receive a 429 Too Many Requests response. The API returns a Retry-After header indicating how many seconds to wait:
import time
import requests
def get_ip_with_retry(max_retries: int = 3) -> str | None:
for attempt in range(max_retries):
response = requests.get(
'https://ipv4.ippubblico.org/',
timeout=5
)
if response.status_code == 200:
return response.text.strip()
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 20))
print(f"Rate limited. Waiting {retry_after}s before retry {attempt + 1}/{max_retries}")
time.sleep(retry_after)
continue
response.raise_for_status()
return None
In practice, caching results at the application level eliminates most rate limit concerns.
Quick reference
| Need | Endpoint | Response |
|---|---|---|
| IPv4 only | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6 only | https://ipv6.ippubblico.org/ |
2001:db8::1 or NONE
|
| Both protocols | https://ippubblico.org/?text=1 |
IPv4: x\nIPv6: x |
| Full geolocation | https://ippubblico.org/?api=1 |
JSON with country, city, ISP |
Full API documentation: ippubblico.org/docs.html
Conclusion
IPPubblico covers the full range of Python use cases — from a one-liner in a shell script to a properly typed FastAPI dependency with Redis caching. The consistent API across plain text and JSON endpoints means you can start simple and add complexity only when you need it.
The Django middleware and FastAPI dependency patterns shown here are production-ready starting points: they handle client IP extraction behind proxies, cache results to avoid redundant API calls, and fail gracefully when the external service is unavailable.
Using a different approach for IP detection in Python? Share it in the comments.
Top comments (0)