Get the IP Address in Python: Local, Public and Client IP, Validation and Ping

Updated

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

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 wantPythonTypical result
This computer’s local IPUDP socket and getsockname()192.168.1.23, a private address
This computer’s public IPAsk a server which address it sees203.0.113.42, your router’s address
A visitor’s IP in a web apprequest.remote_addr and its Django and FastAPI equivalentsThe client, or your proxy until you configure it
The IP addresses of a hostnamesocket.getaddrinfo()Every IPv4 and IPv6 address in DNS
Check or validate an IPipaddress.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:

Python
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 IPv6

Calling 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

Python
import socket

print(socket.gethostname())                         # e.g. ubuntu-desktop
print(socket.gethostbyname(socket.gethostname()))   # often 127.0.1.1 on Debian and Ubuntu

This 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:

Python
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):

Text
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:

Python
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:

Python
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:

FrameworkClient IPBehind a reverse proxy
Flaskrequest.remote_addrWerkzeug ProxyFix
Djangorequest.META["REMOTE_ADDR"]No built-in setting; use a helper
FastAPI, Starletterequest.client.hostUvicorn --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

Python
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:

Python
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 None

Call 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

Python
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:

Shell
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:

Python
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.google

getaddrinfo() 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.

Python
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
Inputipaddress.ip_address()socket.inet_aton()
192.168.1.23ValidAccepted
2001:db8::1ValidRejected (IPv4 only)
fe80::1%eth0Valid, with scope_id eth0Rejected
256.1.1.1ValueErrorRejected
192.168.001.23ValueError since Python 3.9.5Accepted
127.1ValueErrorAccepted as 127.0.0.1
10.0.0.1 (trailing space)ValueErrorAccepted
10.0.0.0/8ValueError; use ip_network()Rejected
192.168.1.23:8080ValueErrorRejected

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

Python
import ipaddress

ip = ipaddress.ip_address("192.168.1.23")
print(ip.is_private, ip.is_global, ip.is_loopback)  # True False False
Addressis_privateis_globalAlso true
192.168.1.23, 10.0.0.5, 172.20.0.1TrueFalse
8.8.8.8, 80.110.0.1FalseTrue
127.0.0.1TrueFalseis_loopback
169.254.10.20TrueFalseis_link_local
100.64.0.1 (carrier-grade NAT)FalseFalse
203.0.113.7 (documentation range)TrueFalse
224.0.0.1FalseTrueis_multicast
2001:4860:4860::8888FalseTrue
fe80::1TrueFalseis_link_local
fd12:3456:789a::1TrueFalse
::ffff:192.168.1.23TrueFalseipv4_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, not not 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_multicast when 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

Python
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))                          # True

Sort and compare IP addresses

Python
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"))  # True

Strings 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:

Python
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 answers

To check a few of your devices at once, run the pings in threads:

Python
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 1 on Linux and macOS and -n 1 on Windows send one request; the timeout of subprocess.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, and ip_address() keeps input such as -f from reaching ping as an option. For a hostname, resolve it first with resolve() from above.
  • Missing command: minimal containers often have no ping, and subprocess.run() raises FileNotFoundError. On Debian and Ubuntu, install iputils-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:

Python
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"