Get the IP Address in Python: Local, Public and Client IP, Validation and Ping
Short answer
For this computer’s local IP, connect a UDP socket to any outside address and read getsockname()[0]; no packet is sent, and unlike gethostbyname(gethostname()) it does not return a loopback address such as 127.0.1.1. The public IP belongs to your router, so ask a server which address it sees, for example an IP lookup API called without an ip parameter. In a web app, read request.remote_addr in Flask, request.META[“REMOTE_ADDR”] in Django or request.client.host in FastAPI, and configure your trusted proxies when the app runs behind one.
On this page
- Which IP address do you need?
- Get your local IP address in Python
- Get your public IP address in Python
- Get the client IP address in Flask, Django and FastAPI
- Get the IP address of a hostname
- Validate an IP address in Python with the ipaddress module
- Ping an IP address from Python
- Get the location of an IP address in Python
In Python, “get the IP address” can mean four things: the local IP of the machine running your code, its public IP, the IP of a visitor to your web app, or an address you need to check. Each needs its own method, and the popular socket.gethostbyname(socket.gethostname()) one-liner often returns a loopback address instead of the local IP. Below is code for each case, mostly from the standard library, plus validation, private and public checks, and ping. For other languages, see the developer guides.
Which IP address do you need?
| You want | Python | Typical result |
|---|---|---|
| This computer’s local IP | UDP socket and getsockname() | 192.168.1.23, a private address |
| This computer’s public IP | Ask a server which address it sees | 203.0.113.42, your router’s address |
| A visitor’s IP in a web app | request.remote_addr and its Django and FastAPI equivalents | The client, or your proxy until you configure it |
| The IP addresses of a hostname | socket.getaddrinfo() | Every IPv4 and IPv6 address in DNS |
| Check or validate an IP | ipaddress.ip_address() | An address object, or ValueError |
Your router gives each device its own local (private) IP address and shares one public IP among all of them. That’s why, on a typical home network, the public IPv4 address appears on none of your computer’s network adapters.
Get your local IP address in Python
This standard-library function asks the operating system which address it would use to reach the internet:
import socket
def local_ip(family=socket.AF_INET):
"""Return the address of the interface used for outgoing traffic, or None."""
target = "192.0.2.1" if family == socket.AF_INET else "2001:db8::1" # documentation addresses
try:
with socket.socket(family, socket.SOCK_DGRAM) as s:
s.connect((target, 80)) # UDP: connect() sends nothing, the OS only picks a route
return s.getsockname()[0]
except OSError: # no network, no route, or no IPv6 on this machine
return None
print(local_ip()) # e.g. 192.168.1.23
print(local_ip(socket.AF_INET6)) # e.g. 2001:db8:1f70::23, or None without IPv6Calling connect() on a UDP socket sends no packet. The OS only looks up the route, and getsockname() returns that route’s source address: the IP of the Wi-Fi, Ethernet or VPN adapter your traffic leaves through. The targets 192.0.2.1 and 2001:db8::1 are reserved documentation addresses, so nothing depends on a real server. Without a network or an IPv6 route, the function returns None.
On a home network, the IPv4 result is a private address. Over IPv6 it’s usually a global address that servers see as well, because IPv6 home networks rarely use NAT. On a server whose interface holds a public IPv4 address, as is common with VPS hosting, the result is the public IP.
Why gethostbyname(gethostname()) returns 127.0.1.1
import socket
print(socket.gethostname()) # e.g. ubuntu-desktop
print(socket.gethostbyname(socket.gethostname())) # often 127.0.1.1 on Debian and UbuntuThis resolves the computer’s own name, so it returns whatever the hosts file or DNS says about that name. Debian and Ubuntu map the hostname to 127.0.1.1 in /etc/hosts on systems without a permanent IP address, so you get a loopback address that only reaches the machine itself. Other systems may return 127.0.0.1 or the address of the wrong adapter, or raise socket.gaierror if the name doesn’t resolve. gethostbyname() also handles IPv4 only, as the socket documentation notes.
List all network interfaces and their IP addresses
The standard library has no portable way to list each interface’s addresses; socket.if_nameindex() returns only the names. The usual choice is the third-party psutil package:
import ipaddress
import socket
import psutil # pip install psutil
stats = psutil.net_if_stats()
for name, addresses in psutil.net_if_addrs().items():
if name not in stats or not stats[name].isup:
continue # skip interfaces that are down
for a in addresses:
if a.family not in (socket.AF_INET, socket.AF_INET6):
continue # skip MAC addresses (psutil.AF_LINK)
ip = ipaddress.ip_address(a.address)
if not ip.is_loopback:
print(f"{name:<10} IPv{ip.version} {ip}{' (link-local)' if ip.is_link_local else ''}")Example output on a Linux laptop (example addresses):
wlp2s0 IPv4 192.168.1.23
wlp2s0 IPv6 2001:db8:1f70::23
wlp2s0 IPv6 fe80::200:5eff:fe00:5301%wlp2s0 (link-local)For one interface, read psutil.net_if_addrs()["wlp2s0"]. Names differ by OS: eth0, enp3s0 or wlp2s0 on Linux, en0 on macOS, Ethernet or Wi-Fi on Windows. On Linux, ip -j addr prints the same data as JSON for json.loads() if you can’t install packages. Older answers recommend netifaces, but it has had no release since 2021, and its PyPI page asks for a new maintainer.
Get your public IP address in Python
On a home network, your public IPv4 address isn’t on your computer: it belongs to your router, or with carrier-grade NAT to your provider. Code learns it by asking a server which address a request came from. The box shows what servers see for your connection right now:
Your public IP address
216.73.216.128
- Location
- Columbus, Ohio, United States
- Provider
- Anthropic, PBC (AS16509)
- Time zone
- America/New_York (UTC−4)
- Coordinates
- 39.96, -83.00
This is the address websites see, and the location is an estimate that can be off by a city or more. Your device's local IP (often 192.168.x.x) is a different one and only works inside your network.
Code on the same network gets the same address, unless one connection uses IPv6 and the other IPv4, or a VPN or proxy sits in between.
The info endpoint of iplookupapi answers for the caller when you leave out the ip parameter. A free API key works; keep it in the IPLOOKUPAPI_KEY environment variable:
import os
import requests
def public_ip(timeout=5):
"""Return the public IP address that servers on the internet see for this machine."""
resp = requests.get(
"https://api.iplookupapi.com/v1/info",
params={"fields": "ip"}, # no "ip" parameter: the API answers for the caller
headers={"apikey": os.environ["IPLOOKUPAPI_KEY"]},
timeout=timeout,
)
resp.raise_for_status()
return resp.json()["data"]["ip"]
try:
print(public_ip()) # e.g. 203.0.113.42
except requests.RequestException as exc: # timeout, no connection, HTTP 401 or 429
print(f"Could not get the public IP: {exc}")fields=ip keeps the response small. Always pass a timeout: requests has none by default, so a stalled connection can block your script for minutes. If you can’t install requests, the standard library works too:
import json
import os
import urllib.request
req = urllib.request.Request(
"https://api.iplookupapi.com/v1/info?fields=ip",
headers={"apikey": os.environ["IPLOOKUPAPI_KEY"]},
)
with urllib.request.urlopen(req, timeout=5) as resp: # raises HTTPError, URLError or TimeoutError
print(json.load(resp)["data"]["ip"])Note
Without an HTTP API, a DNS query also returns your public IPv4 address: run dig -4 +short myip.opendns.com @resolver1.opendns.com with subprocess.run(). It needs dig installed and outbound DNS allowed.
Get the client IP address in Flask, Django and FastAPI
In a web app, the visitor’s IP comes from the connection:
| Framework | Client IP | Behind a reverse proxy |
|---|---|---|
| Flask | request.remote_addr | Werkzeug ProxyFix |
| Django | request.META["REMOTE_ADDR"] | No built-in setting; use a helper |
| FastAPI, Starlette | request.client.host | Uvicorn --forwarded-allow-ips |
Behind nginx, a load balancer or a CDN, the connection comes from the proxy, such as 127.0.0.1 or 10.0.1.23, and the visitor’s IP arrives in the X-Forwarded-For header. Any client can send that header with a fake value, so trust it only on requests from your own proxy. nginx adds it only when you set proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. The guide to the real client IP behind proxies and CDNs covers Cloudflare, AWS, nginx and Apache.
Flask: request.remote_addr and ProxyFix
from flask import Flask, request
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) # exactly one proxy, such as nginx, in front
@app.get("/ip")
def show_ip():
return {"ip": request.remote_addr}x_for=1 means one proxy appends to X-Forwarded-For, so ProxyFix takes the last entry. It doesn’t check who connected, so bind the app server to 127.0.0.1 or to a network only the proxy can reach.
Django: REMOTE_ADDR and a trusted-proxy helper
Django puts the connecting address in request.META["REMOTE_ADDR"] and has no setting for trusted proxies. This helper reads X-Forwarded-For only when the request comes from one of your proxies, and returns the right-most address that none of them added:
import ipaddress
# Your reverse proxies and load balancers, nothing else
TRUSTED_PROXIES = [ipaddress.ip_network(n) for n in ("127.0.0.1/32", "::1/128", "10.0.0.0/8")]
def _parse(value):
try:
ip = ipaddress.ip_address(value.strip())
except ValueError:
return None
return getattr(ip, "ipv4_mapped", None) or ip # ::ffff:203.0.113.7 -> 203.0.113.7
def client_ip(request):
"""REMOTE_ADDR, or the right-most X-Forwarded-For entry that your proxies did not add."""
ip = _parse(request.META.get("REMOTE_ADDR", ""))
hops = [h for h in request.META.get("HTTP_X_FORWARDED_FOR", "").split(",") if h.strip()]
while ip is not None and hops and any(ip in net for net in TRUSTED_PROXIES):
ip = _parse(hops.pop())
return str(ip) if ip is not None else NoneCall client_ip(request) in a view or middleware. It returns None when the entry it needs is not a plain IP address, such as one with a port.
FastAPI: request.client.host and Uvicorn’s trusted proxies
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/ip")
def show_ip(request: Request):
return {"ip": request.client.host if request.client else None}Uvicorn applies X-Forwarded-For only to connections from --forwarded-allow-ips, by default 127.0.0.1 and ::1, so nginx on the same host needs no extra setting. For a proxy on another machine, list its address or subnet; fastapi run takes the same option:
uvicorn main:app --host 0.0.0.0 --forwarded-allow-ips="10.0.0.5"Uvicorn then uses the right-most untrusted entry, but it doesn’t check that the entry is an IP address, so validate request.client.host with ipaddress.ip_address() before you store it. Use --forwarded-allow-ips="*" only if nothing but your proxy can reach the app: Uvicorn then returns the left-most entry, which the client controls.
When you test on your own machine, you get 127.0.0.1 or ::1. Under the GDPR, an IP address is usually personal data, so keep visitor IPs only as long as you need them, or store only the country you derive from them. For Node.js and the browser, see getting the IP address in JavaScript.
Get the IP address of a hostname
The system resolver returns every IPv4 and IPv6 address of a name:
import socket
def resolve(hostname):
"""All IPv4 and IPv6 addresses DNS returns for a hostname, without duplicates."""
infos = socket.getaddrinfo(hostname, None, type=socket.SOCK_STREAM)
return list(dict.fromkeys(info[4][0] for info in infos))
print(resolve("example.com")) # IPv6 and IPv4 addresses as strings
print(socket.gethostbyname("example.com")) # one IPv4 address only
print(socket.gethostbyaddr("8.8.8.8")[0]) # reverse DNS (PTR record): dns.googlegetaddrinfo() raises socket.gaierror if the name doesn’t resolve, and gethostbyaddr() raises socket.herror when an address has no reverse DNS entry.
Validate an IP address in Python with the ipaddress module
The ipaddress module validates IPv4 and IPv6: ip_address() returns an address object or raises ValueError.
import ipaddress
def is_valid_ip(value):
"""True for a valid IPv4 or IPv6 address string, False for anything else."""
if not isinstance(value, str):
return False # ip_address() would also accept integers and bytes
try:
ipaddress.ip_address(value)
except ValueError:
return False
return True
print(is_valid_ip("192.168.1.23")) # True
print(is_valid_ip("2001:db8::1")) # True
print(is_valid_ip("192.168.001.23")) # False: leading zeros (Python 3.9.5 and later)
print(is_valid_ip("256.1.1.1")) # False| Input | ipaddress.ip_address() | socket.inet_aton() |
|---|---|---|
192.168.1.23 | Valid | Accepted |
2001:db8::1 | Valid | Rejected (IPv4 only) |
fe80::1%eth0 | Valid, with scope_id eth0 | Rejected |
256.1.1.1 | ValueError | Rejected |
192.168.001.23 | ValueError since Python 3.9.5 | Accepted |
127.1 | ValueError | Accepted as 127.0.0.1 |
10.0.0.1 (trailing space) | ValueError | Accepted |
10.0.0.0/8 | ValueError; use ip_network() | Rejected |
192.168.1.23:8080 | ValueError | Rejected |
Don’t validate with socket.inet_aton(): it accepts shorthand such as 127.1 and a trailing space. Hand-written regular expressions often miss IPv6 or accept 999.1.1.1. Strip whitespace yourself if your input may contain it. To allow only one version, use ipaddress.IPv4Address() or IPv6Address(), which raise AddressValueError, a subclass of ValueError.
Check if an IP address is private or public
import ipaddress
ip = ipaddress.ip_address("192.168.1.23")
print(ip.is_private, ip.is_global, ip.is_loopback) # True False False| Address | is_private | is_global | Also true |
|---|---|---|---|
| 192.168.1.23, 10.0.0.5, 172.20.0.1 | True | False | |
| 8.8.8.8, 80.110.0.1 | False | True | |
| 127.0.0.1 | True | False | is_loopback |
| 169.254.10.20 | True | False | is_link_local |
| 100.64.0.1 (carrier-grade NAT) | False | False | |
| 203.0.113.7 (documentation range) | True | False | |
| 224.0.0.1 | False | True | is_multicast |
| 2001:4860:4860::8888 | False | True | |
| fe80::1 | True | False | is_link_local |
| fd12:3456:789a::1 | True | False | |
| ::ffff:192.168.1.23 | True | False | ipv4_mapped is 192.168.1.23 |
The values follow IANA’s special-purpose address registries as implemented since Python 3.13, which corrected several ranges; older versions can differ. Three results surprise people:
- To test for a public address, use
is_global, notnot is_private: the carrier-grade NAT range 100.64.0.0/10 is neither. - Documentation ranges such as 203.0.113.0/24 and 2001:db8::/32 count as private, so they make poor sample public addresses in tests.
- Multicast addresses such as 224.0.0.1 are global; add
not ip.is_multicastwhen you need a public unicast address.
Dual-stack servers can report IPv4 clients as IPv4-mapped IPv6 addresses such as ::ffff:192.0.2.7. ip.ipv4_mapped returns the IPv4 address inside, and None for other IPv6 addresses.
Work with networks and subnets
import ipaddress
net = ipaddress.ip_network("192.168.1.0/24")
print(ipaddress.ip_address("192.168.1.23") in net) # True
print(net.netmask, net.broadcast_address, net.num_addresses) # 255.255.255.0 192.168.1.255 256
print(next(net.hosts()), list(net.hosts())[-1]) # 192.168.1.1 192.168.1.254
# An address with a prefix, as `ip addr` prints it
iface = ipaddress.ip_interface("192.168.1.23/24")
print(iface.ip, iface.network) # 192.168.1.23 192.168.1.0/24
# ip_network("192.168.1.23/24") raises ValueError (host bits set); strict=False masks them
print(ipaddress.ip_network("192.168.1.23/24", strict=False)) # 192.168.1.0/24
# Allowlist with IPv4 and IPv6 networks; an IPv4 address never matches an IPv6 network
allowed = [ipaddress.ip_network(n) for n in ("10.0.0.0/8", "2001:db8::/32")]
ip = ipaddress.ip_address("2001:db8::7")
print(any(ip in n for n in allowed)) # TrueSort and compare IP addresses
import ipaddress
ips = ["10.0.0.10", "2001:db8::1", "10.0.0.9", "192.168.1.1"]
print(sorted(ips)) # string order: ['10.0.0.10', '10.0.0.9', '192.168.1.1', '2001:db8::1']
addrs = [ipaddress.ip_address(s) for s in ips]
# sorted(addrs) raises TypeError, because IPv4 and IPv6 objects can't be compared
print([str(a) for a in sorted(addrs, key=lambda a: (a.version, a))])
# ['10.0.0.9', '10.0.0.10', '192.168.1.1', '2001:db8::1']
print(int(ipaddress.ip_address("192.168.1.1"))) # 3232235777
print(ipaddress.ip_address(3232235777)) # 192.168.1.1
print(ipaddress.ip_address("2001:db8:0:0:0:0:0:1") == ipaddress.ip_address("2001:db8::1")) # TrueStrings sort character by character, so 10.0.0.10 lands before 10.0.0.9. Address objects sort numerically and compare equal however they’re written.
Ping an IP address from Python
Python has no built-in ping. Sending ICMP yourself needs a raw socket, which usually requires root or administrator rights, so the portable way is to run the system ping command:
import ipaddress
import platform
import subprocess
def ping(ip, timeout=3):
"""Send one echo request. True if the address answered within timeout seconds."""
addr = ipaddress.ip_address(ip) # ValueError for hostnames, typos and "-x" style options
system = platform.system()
cmd = ["ping", "-n" if system == "Windows" else "-c", "1", str(addr)]
if system == "Darwin" and addr.version == 6:
cmd[0] = "ping6" # macOS has a separate ping6 for IPv6
try:
result = subprocess.run(cmd, capture_output=True, timeout=timeout)
except subprocess.TimeoutExpired:
return False # no answer in time; run() has already stopped ping
if system == "Windows" and addr.version == 4:
# Windows also exits with 0 when a router replies "Destination host unreachable"
return result.returncode == 0 and b"TTL=" in result.stdout
return result.returncode == 0
print(ping("192.168.1.1")) # True if your router answersTo check a few of your devices at once, run the pings in threads:
from concurrent.futures import ThreadPoolExecutor
devices = ["192.168.1.1", "192.168.1.20", "192.168.1.25"] # router, NAS, printer
with ThreadPoolExecutor(max_workers=8) as pool:
for ip, up in zip(devices, pool.map(ping, devices)):
print(ip, "answers" if up else "no answer")How the function deals with each platform:
- Count and timeout:
-c 1on Linux and macOS and-n 1on Windows send one request; thetimeoutofsubprocess.run()stops ping when no answer comes. - Return codes: 0 means a reply. Linux ping exits with 1 without a reply and 2 on other errors; macOS exits with 2 without a reply. Windows can exit with 0 when a router answers for an unreachable host, hence the
TTL=check for IPv4. - Safety: the list form of
subprocess.run()uses no shell, andip_address()keeps input such as-ffrom reaching ping as an option. For a hostname, resolve it first withresolve()from above. - Missing command: minimal containers often have no ping, and
subprocess.run()raisesFileNotFoundError. On Debian and Ubuntu, installiputils-ping.
No answer doesn’t prove a device is offline, since many firewalls block ping. Ping only devices on networks you own or manage. The guide on how to ping an IP address explains ping’s output and errors on each OS.
Get the location of an IP address in Python
To turn a public IP into a country, city, network and time zone, send it to the iplookupapi info endpoint and ask only for the fields you need:
import ipaddress
import os
import requests
FIELDS = "ip,location.country.alpha2,location.city.name,connection.asn,timezone.id"
def ip_location(ip):
"""Country, city, network and time zone of a public IP; None for other addresses."""
if not ipaddress.ip_address(ip).is_global:
return None # 192.168.x.x, 127.0.0.1 and other non-public addresses have no location
resp = requests.get(
"https://api.iplookupapi.com/v1/info",
params={"ip": ip, "fields": FIELDS},
headers={"apikey": os.environ["IPLOOKUPAPI_KEY"]},
timeout=5,
)
resp.raise_for_status()
return resp.json()["data"]
info = ip_location("80.110.0.1")
print(info["location"]["country"]["alpha2"], info["location"]["city"]["name"],
info["connection"]["asn"], info["timezone"]["id"])With the documentation’s sample response for 80.110.0.1, this prints AT Vienna 8412 Europe/Vienna. The is_global check skips private addresses, which have no location, and saves a request. The location is an estimate for the network, not a street address. For the official Python package, retries, caching and framework recipes, see IP geolocation in Python; to look up as many as 1,000 addresses in one request, use the batch IP lookup API.
Frequently asked questions
How do I get my IP address in Python?
For the local IP of the machine running your script, connect a UDP socket to an outside address and read getsockname()[0]: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM), then s.connect((‘192.0.2.1’, 80)) and print(s.getsockname()[0]). The connect call sends no packet, and the result is the address of the adapter your traffic leaves through, such as 192.168.1.23. Your public IP belongs to your router, so you get it only by asking a server which address your request came from.
How do I get my public IP address in Python?
Ask a server which address your request came from, because the public IP sits on your router or your provider’s network, not on your computer. With iplookupapi, call requests.get(‘https://api.iplookupapi.com/v1/info', params={‘fields’: ‘ip’}, headers={‘apikey’: key}, timeout=5) and read .json()[‘data’][‘ip’]. Without an HTTP API, dig -4 +short myip.opendns.com @resolver1.opendns.com, run through subprocess, returns your public IPv4 address.
Why does socket.gethostbyname(socket.gethostname()) return 127.0.1.1?
Debian and Ubuntu map the computer’s hostname to 127.0.1.1 in /etc/hosts when the system has no permanent IP address, and gethostbyname() returns that entry. 127.0.1.1 is a loopback address that only reaches the machine itself. On other systems the same call can return 127.0.0.1 or the address of the wrong adapter, so connect a UDP socket and read getsockname()[0] instead.
How do I validate an IP address in Python?
Pass the string to ipaddress.ip_address() and catch ValueError. It accepts valid IPv4 and IPv6 addresses and rejects everything else, including 256.1.1.1 and, since Python 3.9.5, leading zeros such as 192.168.001.23. For IPv4 only, use ipaddress.IPv4Address(), which raises AddressValueError. Don’t validate with socket.inet_aton(), because it also accepts shorthand such as 127.1.
How do I get the client IP in Flask?
Read request.remote_addr. Behind a reverse proxy such as nginx it holds the proxy’s address until you wrap the app in Werkzeug’s ProxyFix: app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1), where x_for is the number of proxies that append to X-Forwarded-For. ProxyFix does not check who connected, so make sure only your proxy can reach the app. The guide to the real client IP behind proxies covers CDNs and load balancers.
How do I ping an IP address in Python?
Run the system ping command with subprocess.run() and check the return code: subprocess.run([‘ping’, ‘-c’, ‘1’, ‘192.168.1.1’], capture_output=True, timeout=5).returncode == 0 means the address answered. On Windows, use -n instead of -c and also check the output for TTL=, because Windows ping can exit with 0 when a router replies Destination host unreachable. Pass the address as a list item, never through a shell.
Look up IP addresses in your code
iplookupapi returns the location, provider, time zone and security flags of any IP address as JSON. The free plan includes 150 requests a month.
curl "https://api.iplookupapi.com/v1/info?ip=8.8.8.8" \
-H "apikey: YOUR_API_KEY"