You notice it the first time you build anything network-aware: the "IP address" question is not a single question. When you say "my IP," you might mean the private address your laptop uses on Wi-Fi, the address your container uses inside Docker, the IPv6 address your OS prefers, or the public address your router shows to the internet.\n\nI‘ve shipped enough networked software to be suspicious of the first answer any program returns. Your machine can have multiple active interfaces (Wi-Fi + Ethernet + VPN), multiple address families (IPv4 + IPv6), and multiple routes. Add NAT and corporate proxies and you can easily print an address that is technically correct but useless for your real goal.\n\nSo here‘s how I approach it in Python: I start by deciding which category of IP I need (local vs public, IPv4 vs IPv6, specific interface vs default route). Then I choose a method that matches that intent, with predictable behavior across Windows/macOS/Linux and sane failure modes. I‘ll show a few runnable scripts and explain what each one actually returns, where it breaks, and what I recommend in 2026 for modern Python codebases.\n\n## The four IP "types" you‘ll run into in real code\nBefore you write code, lock down the meaning. These are the ones I reach for most often:\n\n1) Local/private IP (LAN)\n- Examples: 192.168.1.35, 10.0.0.12, 172.16.2.20\n- Usually assigned by your router (DHCP) or a VPN.\n- Only reachable inside the private network (unless you do port forwarding).\n\n2) Public/external IP (internet-facing)\n- Example: 203.0.113.42\n- This is what external services see as the source of your outbound traffic.\n- With NAT, many devices share one public IP.\n\n3) Loopback\n- Examples: 127.0.0.1, ::1\n- Only reachable on the same machine.\n- Great for local dev; wrong for "what‘s my LAN IP?"\n\n4) Interface-specific IP\n- You may want "the Wi-Fi adapter IPv4" specifically, not "whatever the default route is."\n- This matters for diagnostics tools, local network discovery, and apps that bind to a chosen interface.\n\nA simple analogy I use: asking for "your IP" without context is like asking for "your address" without saying whether you want your home address, your office, your mailing address, or your GPS coordinates.\n\n## Choosing the right answer (a quick decision checklist)\nWhen I‘m coding this for real, I write down the intent in a sentence and pick a technique that matches it. Here are the questions I ask myself, in order:\n\n1) Do I need a local address or the public address?\n- Local: I can compute it from local interfaces and routing.\n- Public: I must ask an external service over HTTPS (or query my router, which is even more environment-specific).\n\n2) Do I care about IPv4, IPv6, or "whatever works"?\n- If you bind servers, you often need to decide explicitly. IPv6 can be preferred even when IPv4 exists.\n- Some networks (especially mobile and some enterprise) are IPv6-heavy.\n\n3) Do I need the "preferred" address (default route) or a specific interface?\n- Preferred/default route is great for "what will I use to reach the internet?"\n- Interface-specific is what you want for diagnostics, local discovery, or user-driven configuration.\n\n4) Is the environment normal, or is it containerized / VPN / multi-homed?\n- In Docker and Kubernetes, the container has its own network namespace. "My IP" might be a container IP that is not reachable from your laptop.\n- With VPNs, the preferred outbound route might change, and that is usually correct.\n\n5) What are my failure modes?\n- If the machine is offline, do I want a best-effort local IP, or do I want to fail loudly?\n- If the public IP service is down, should the program retry, fall back, or just skip?\n\nThat checklist sounds boring, but it prevents the classic bug where you proudly print an IP address and your user immediately replies, "That‘s not right" (because they meant a different IP category than you assumed).\n\n## Quick local IP via socket.gethostname() (and the traps)\nThe simplest snippet people reach for is "hostname -> IP." It‘s short, it‘s built-in, and it often prints something plausible.\n\nHere‘s the classic version:\n\n import socket\n\n hostname = socket.gethostname()\n ipaddr = socket.gethostbyname(hostname)\n\n print(‘Computer name:‘, hostname)\n print(‘Local IP (hostname-based):‘, ipaddr)\n\nWhy it works sometimes:\n- Your hostname is registered in DNS (common in managed corporate networks).\n- Or your OS maps the hostname to an address that happens to be your LAN IP.\n\nWhy it fails more often than you‘d expect:\n- On many machines, the hostname resolves to a loopback address like 127.0.0.1.\n- On some Linux setups, hostname resolution goes through /etc/hosts first and may map to 127.0.1.1.\n- If you have multiple interfaces, "hostname-based" can pick an arbitrary one.\n- If IPv6 is enabled and preferred, this method still often pushes you into IPv4-only assumptions.\n\nWhen I still use it:\n- Quick demos.\n- Controlled environments where hostnames are correctly registered.\n\nWhen I don‘t:\n- Anything customer-facing.\n- Diagnostics tooling where correctness matters.\n\nIf you only remember one thing from this section: gethostbyname(gethostname()) answers "what does my hostname resolve to?" not "what IP will I use to reach other machines on my LAN?"\n\n## A more reliable local IP: the "UDP connect" trick\nWhen you want the local IP used for outbound traffic, a more reliable approach is: create a UDP socket and "connect" it to a remote address (no packets need to be sent), then ask the socket which local address it selected.\n\nThis is my default for "give me the primary IPv4 I‘ll use for outbound connections."\n\n import socket\n from contextlib import closing\n\n\n def getpreferredlocalipv4() -> str:\n # This does not send data. It forces the OS routing table to pick\n # the outbound interface/address it would use.\n with closing(socket.socket(socket.AFINET, socket.SOCKDGRAM)) as s:\n s.connect((‘8.8.8.8‘, 80))\n return s.getsockname()[0]\n\n\n if name == ‘main‘:\n print(‘Preferred local IPv4:‘, getpreferredlocalipv4())\n\nNotes I care about in production:\n- The IP 8.8.8.8 is just a routing hint. You can replace it with any stable public IPv4.\n- No traffic is required for the local address selection; connect() on UDP sets internal state.\n- If you‘re offline or behind weird policy routing, this can raise an error.\n\nA safer version with fallbacks:\n\n import socket\n from contextlib import closing\n\n\n def getpreferredlocalipv4(fallback: str = ‘127.0.0.1‘) -> str:\n try:\n with closing(socket.socket(socket.AFINET, socket.SOCKDGRAM)) as s:\n s.connect((‘8.8.8.8‘, 80))\n return s.getsockname()[0]\n except OSError:\n return fallback\n\n\n if name == ‘main‘:\n print(getpreferredlocalipv4())\n\nWhat this returns:\n- The local IPv4 bound to the interface the OS would use to reach the internet.\n\nWhat it does not guarantee:\n- That this is your Wi-Fi IP (VPN could win).\n- That you even have IPv4 enabled.\n- That the address is reachable from your peer (for example, you might be behind multiple NAT layers).\n\nIf you want IPv6, you can do the same thing with AFINET6, but the remote address must be IPv6:\n\n import socket\n from contextlib import closing\n\n\n def getpreferredlocalipv6(fallback: str = ‘::1‘) -> str:\n try:\n with closing(socket.socket(socket.AFINET6, socket.SOCKDGRAM)) as s:\n s.connect((‘2001:4860:4860::8888‘, 80))\n return s.getsockname()[0]\n except OSError:\n return fallback\n\n\n if name == ‘main‘:\n print(getpreferredlocalipv6())\n\nPerformance expectations:\n- This is typically extremely fast (often under a few milliseconds) because it‘s just socket setup and route selection.\n\nCommon mistake:\n- Treating the returned value as "public IP." It‘s still a private/local address.\n\n### A nuance that matters in 2026: IPv6 privacy addresses and stability\nIf you start returning IPv6 addresses, expect surprises. Many operating systems use temporary IPv6 addresses (privacy extensions) that rotate periodically. That can be good for user privacy, but it means:\n- The IPv6 you print today might not be the IPv6 you print tomorrow.\n- A machine can have multiple global IPv6 addresses simultaneously (stable + temporary).\n\nWhen I‘m writing tooling, I often print "all IPv6 addresses" and then separately print "preferred IPv6 for outbound" using the UDP trick above. That makes the output explain itself.\n\n### What if I don‘t want to hardcode 8.8.8.8?\nHardcoding a well-known IP is fine for route selection, but sometimes you want to avoid it for policy or style reasons. My compromise is:\n- Pick a reserved documentation address (like 198.51.100.1) for local route selection.\n- Accept that some stacks might treat it differently than a real public IP (rare, but possible).\n\nIn practice, the real-world difference is minimal, and the biggest risk is not the choice of remote IP but the assumption that "preferred outbound" equals "the IP I want."\n\n## Enumerating all interface IPs (Wi-Fi vs Ethernet vs VPN vs Docker)\nIf you‘re writing tooling (or any app that binds a server socket), you‘ll eventually want the full list of addresses by interface.\n\nIn 2026, I still see two practical choices:\n- psutil (more common in ops/diagnostics stacks; supports lots of system info)\n- netifaces (smaller, focused on interfaces)\n\nIf you can add one dependency, I generally recommend psutil because you‘ll likely want process, memory, and network stats later anyway.\n\n### Option A (my default): psutil.netifaddrs()\nInstall:\n- python -m pip install psutil\n\nCode:\n\n import socket\n import psutil\n\n\n def iterinterfaceips():\n for ifname, addrs in psutil.netifaddrs().items():\n for addr in addrs:\n if addr.family == socket.AFINET:\n yield ifname, ‘ipv4‘, addr.address\n elif addr.family == socket.AFINET6:\n # Some platforms include a scope id like ‘%en0‘ in the address.\n yield ifname, ‘ipv6‘, addr.address\n\n\n if name == ‘main‘:\n for ifname, family, ip in iterinterfaceips():\n print(f‘{ifname:15} {family:4} {ip}‘)\n\nWhat I like about this:\n- You get a reliable list even when hostname resolution lies.\n- It works well on laptops with Wi-Fi + VPN + virtual interfaces.\n\nWhat you still need to decide:\n- Which interface counts as "wireless" on the current OS.\n- Whether you want to filter out link-local IPv6 (fe80::/10).\n- Whether to filter out addresses on virtual adapters (Docker bridges, VM networks) for your use case.\n\n### Option B: netifaces for a smaller dependency\nInstall:\n- python -m pip install netifaces\n\nCode (find a likely Wi-Fi interface name):\n\n import netifaces\n\n\n def getwirelessipv4() -> str
wlan0, wlp2s0, or similar.\n- macOS often uses en0 for Wi-Fi (not obvious from the name).\n\nIf you need "Wi-Fi only" cross-platform, I usually combine:\n- Interface "is up" + has default route + is not a known virtual adapter\n- Or I let the user pick from a list (best UX for tooling)\n\n### Practical filter: skip obvious non-routable addresses\nWhen listing addresses, I typically ignore:\n- Loopback (127.0.0.1, ::1)\n- IPv6 link-local (fe80::...) unless I‘m doing low-level LAN discovery\n\nHere‘s a small helper using the standard library ipaddress module:\n\n import ipaddress\n\n\n def isusefulip(ip: str) -> bool:\n try:\n obj = ipaddress.ipaddress(ip.split(‘%‘)[0]) # strip IPv6 scope if present\n except ValueError:\n return False\n\n if obj.isloopback:\n return False\n if obj.version == 6 and obj.islinklocal:\n return False\n\n return True\n\nI also sometimes filter out:\n- IPv4 link-local (169.254.0.0/16), which appears when DHCP fails\n- Multicast and unspecified addresses\n\nIf you‘re curious, ipaddress.ipaddress(...) has flags like isprivate, isglobal, ismulticast, and isunspecified that help you build consistent rules.\n\n### Why the standard library alone is not great for full interface enumeration\nPeople ask, "Can I do this without dependencies?" You can do some things, but full interface enumeration is the point where the standard library starts to feel intentionally low-level.\n\nWhat the standard library does well:\n- Ask the OS routing system for the chosen source address for a connection (UDP connect trick).\n- Resolve names and service addresses (DNS and getaddrinfo).\n\nWhat it doesn‘t give you in a clean, portable way:\n- A mapping of interface name -> list of addresses.\n\nSo my rule of thumb is:\n- If you just need the preferred local address: use socket only.\n- If you need interface lists for diagnostics or UI: use psutil (or a platform-specific approach if dependencies are forbidden).\n\n## Getting the public IP safely (without brittle regex)\nIf you need the public IP, your machine cannot magically know it without asking something outside your network. With NAT, only your router (or your VPN provider) knows what the internet sees.\n\nSo the correct approach is: call a trusted "what is my IP" endpoint over HTTPS and parse the result.\n\nI prefer JSON endpoints because they‘re stable and don‘t require regex.\n\n import json\n from urllib.request import urlopen, Request\n\n\n def getpublicip(timeouts: float = 5.0) -> str:\n req = Request(\n ‘https://api.ipify.org?format=json‘,\n headers={\n ‘User-Agent‘: ‘python-public-ip/1.0‘\n },\n )\n\n with urlopen(req, timeout=timeouts) as resp:\n data = json.loads(resp.read().decode(‘utf-8‘))\n return data[‘ip‘]\n\n\n if name == ‘main‘:\n print(‘Public IP:‘, getpublicip())\n\nWhy I‘m picky about HTTPS:\n- Public IP endpoints are often used as a building block in security-sensitive workflows.\n- Plain HTTP lets anyone on the path spoof results.\n\nWhat you should expect:\n- The returned public IP may be your VPN‘s egress IP if a VPN is active.\n- In some enterprise networks, outbound traffic may go through proxies with different egress behavior.\n\nPerformance expectations:\n- Typically tens to a few hundred milliseconds depending on latency.\n\nCommon mistakes:\n- Doing this on every request in a server app (you‘ll DDoS your dependency and slow yourself down).\n- Treating a public IP as a stable identifier for a user. It isn‘t.\n\nIf you need resilience, add fallbacks across multiple endpoints and short timeouts. Example:\n\n import json\n from urllib.request import urlopen\n\n\n def getpublicip(timeouts: float = 3.0) -> str:\n candidates = [\n ‘https://api.ipify.org?format=json‘,\n ‘https://ifconfig.me/all.json‘,\n ]\n\n lasterr = None\n for url in candidates:\n try:\n with urlopen(url, timeout=timeouts) as resp:\n payload = resp.read().decode(‘utf-8‘)\n data = json.loads(payload)\n\n # Different services return different keys; handle a couple common ones.\n if ‘ip‘ in data:\n return data[‘ip‘]\n if ‘ipaddr‘ in data:\n return data[‘ipaddr‘]\n except Exception as e:\n lasterr = e\n\n raise RuntimeError(f‘Unable to determine public IP: {lasterr}‘)\n\n### Caching and rate limiting: the production detail everyone forgets\nIf I‘m writing a CLI tool, it‘s fine to call a public endpoint on demand. If I‘m writing a service, I almost never want to call a public IP endpoint frequently.\n\nA simple, practical pattern:\n- Cache the public IP for a short TTL (for example, 5 to 30 minutes).\n- Provide a manual refresh option.\n- Use a very small timeout (like 1 to 3 seconds) and do not retry endlessly.\n\nHere‘s a tiny cache wrapper that stays in-process (no files, no dependencies):\n\n import time\n\n\n class PublicIPCache:\n def init(self, ttls: float = 600.0):\n self.ttls = ttls\n self.value: str None = None\n self.expiresat = 0.0\n\n def get(self) -> str
ipconfig / ifconfig / ip (when you must)\nSometimes you need to match what humans see in the terminal, or you‘re in an environment where Python can‘t access certain details without elevated permissions. Running OS commands is valid, but I avoid os.system().\n\nos.system():\n- mixes stdout/stderr with your program output\n- only returns an exit code\n- is awkward to parse\n\nI prefer subprocess.run() because you control output and errors.\n\n### Windows: ipconfig\n\n import subprocess\n\n\n def showipconfig() -> str:\n result = subprocess.run(\n [‘ipconfig‘],\n captureoutput=True,\n text=True,\n check=False,\n )\n return result.stdout\n\n\n if name == ‘main‘:\n print(showipconfig())\n\n### Linux: ip addr (more modern than ifconfig)\n\n import subprocess\n\n\n def showipaddr() -> str:\n result = subprocess.run(\n [‘ip‘, ‘addr‘],\n captureoutput=True,\n text=True,\n check=False,\n )\n return result.stdout\n\n\n if name == ‘main‘:\n print(showipaddr())\n\n### macOS: ifconfig is still common\n\n import subprocess\n\n\n def showifconfig() -> str:\n result = subprocess.run(\n [‘ifconfig‘],\n captureoutput=True,\n text=True,\n check=False,\n )\n return result.stdout\n\n\n if name == ‘main‘:\n print(showifconfig())\n\nWhen I recommend shelling out:\n- You‘re building a diagnostics CLI and you want parity with OS tools.\n- You need extra fields (routes, MTU, flags) and you‘re already parsing command output.\n\nWhen I don‘t:\n- Any environment where command availability varies (minimal containers, restricted PATH).\n- Any app where output parsing becomes a maintenance burden.\n\nSecurity note:\n- Never pass untrusted input into subprocess command lists.\n- Avoid shell=True unless you have a strong reason.\n\n## Binding servers correctly (the "I printed an IP but I still can‘t connect" problem)\nA surprisingly common reason people ask for an IP address is not to print it, but to make a server reachable. This is where "which IP" becomes "which bind address."\n\nA few rules I rely on:\n\n1) 127.0.0.1 means "only this machine."\n- If you bind to loopback, your phone on the same Wi-Fi will not be able to connect.\n\n2) 0.0.0.0 means "all IPv4 interfaces."\n- Great for dev servers and containers.\n- It is not a real destination address; it‘s a binding wildcard.\n\n3) :: means "all IPv6 interfaces."\n- On some OSes, binding to :: may also accept IPv4 connections (dual-stack) but do not assume it.\n\n4) Printing a LAN IP does not automatically make your server listen there.\n- You can print 192.168.1.35 all day, but if your server is bound to 127.0.0.1, it‘s still unreachable from other devices.\n\nHere‘s a minimal HTTP server that binds explicitly and prints what it is doing (use this when debugging):\n\n import http.server\n import socketserver\n\n\n def runserver(host: str = ‘0.0.0.0‘, port: int = 8000) -> None:\n handler = http.server.SimpleHTTPRequestHandler\n with socketserver.TCPServer((host, port), handler) as httpd:\n print(f‘Listening on {host}:{port}‘)\n httpd.serveforever()\n\n\n if name == ‘main‘:\n runserver()\n\nAnd here‘s the mental model I use for app UX:\n- Bind to 0.0.0.0 (or ::) when the goal is reachability.\n- Then print a list of candidate client URLs based on interface IPs (like http://192.168.1.35:8000/).\n\nThat approach avoids the mistake of binding to a single interface in a multi-homed environment and accidentally breaking users on VPNs or multiple NICs.\n\n## Working inside Docker, containers, and Kubernetes (what "my IP" means there)\nIf you run the same Python script on your laptop and inside a container, the answers may legitimately differ. That‘s not Python being weird; that‘s networking being namespaced.\n\nIn Docker default bridge networking, a container typically has:\n- A private container IP on a bridge network (often 172.17.x.x or similar).\n- No direct LAN presence unless you publish ports (-p 8000:8000) or use host networking.\n\nSo if your Python program prints 172.17.0.2, that might be correct for the container, but it‘s not the address your browser should use from your host machine. From the host, you‘ll likely use localhost:8000 (with port publishing) or the host‘s LAN IP.\n\nWhat I do in container-friendly apps:\n- I bind to 0.0.0.0 inside the container.\n- I avoid promising a specific reachable IP unless I know the deployment model.\n- If I print "connect here" hints, I make them conditional (for example, "If running locally, try http://localhost:8000").\n\nIn Kubernetes, it gets even more layered:\n- Pods have pod IPs (cluster-internal).\n- Services have virtual IPs.\n- Ingresses and load balancers expose public or edge-facing IPs.\n\nIn that world, asking the pod for "its IP" is rarely what users want. Usually they want the Service or Ingress address, which is not something a random pod should guess. In other words: sometimes the right fix is not "better Python IP code" but "use service discovery and configuration."\n\n## Practical scenarios and what I recommend\nThis is the part I wish more tutorials included: pick your scenario first, then pick the smallest reliable method.\n\n### Scenario 1: "I need to show the user which LAN IP to use to reach my dev server"\nMy approach:\n- Bind server to 0.0.0.0.\n- Enumerate interface IPs (via psutil) and print a short list of likely candidates.\n- Filter out loopback and link-local IPv6 by default.\n- Let the user choose if multiple remain.\n\n### Scenario 2: "I need the source IP used for outbound connections"\nMy approach:\n- Use the UDP connect trick for IPv4 and/or IPv6.\n- Treat the result as "preferred local" (not "public").\n\n### Scenario 3: "I need the public IP for a one-time diagnostic"\nMy approach:\n- Call a reputable HTTPS JSON endpoint with a short timeout.\n- Print it, but also print a note if a VPN might affect it.\n\n### Scenario 4: "I need to log IP addresses for auditing"\nMy approach:\n- Be explicit: log local interface IPs and the public egress IP separately.\n- Do not assume either is stable across time.\n- Consider privacy: IPs can be personal data depending on jurisdiction and context.\n\n### Scenario 5: "I need to bind to a specific interface"\nMy approach:\n- Enumerate interfaces and present options.\n- Avoid guessing Wi-Fi by name unless you control the environment.\n- Store the chosen bind address or interface name in config.\n\n## Putting it together: a small, modern "IP inspector" script\nIf you want one runnable program you can drop into a repo, here‘s a version that:\n- prints the preferred local IPv4 and IPv6\n- prints the public IP (best-effort)\n- lists all interface IPs (optional dependency)\n- filters obvious junk\n- can emit JSON for tooling\n\nThis targets Python 3.10+ for str None. If you‘re on Python 3.9, replace those with Optional[str] from typing.\n\n import argparse\n import ipaddress\n import json\n import socket\n from contextlib import closing\n from urllib.request import Request, urlopen\n\n\n def getpreferredlocalipv4(fallback: str = ‘127.0.0.1‘) -> str:\n try:\n with closing(socket.socket(socket.AFINET, socket.SOCKDGRAM)) as s:\n s.connect((‘8.8.8.8‘, 80))\n return s.getsockname()[0]\n except OSError:\n return fallback\n\n\n def getpreferredlocalipv6(fallback: str = ‘::1‘) -> str:\n try:\n with closing(socket.socket(socket.AFINET6, socket.SOCKDGRAM)) as s:\n s.connect((‘2001:4860:4860::8888‘, 80))\n return s.getsockname()[0]\n except OSError:\n return fallback\n\n\n def isusefulip(ip: str, *, allowlinklocalv6: bool = False) -> bool:\n # ip may include an IPv6 zone id like ‘%en0‘.\n raw = ip.split(‘%‘)[0]\n try:\n obj = ipaddress.ipaddress(raw)\n except ValueError:\n return False\n\n if obj.isloopback or obj.isunspecified or obj.ismulticast:\n return False\n\n if obj.version == 6 and obj.islinklocal and not allowlinklocalv6:\n return False\n\n # I usually drop 169.254.0.0/16 unless I‘m debugging DHCP issues.\n if obj.version == 4 and obj.islinklocal:\n return False\n\n return True\n\n\n def listinterfaceips() -> list[dict]:\n # Optional: only works if psutil is installed.\n try:\n import psutil\n except Exception:\n return []\n\n out: list[dict] = []\n for ifname, addrs in psutil.netifaddrs().items():\n for addr in addrs:\n if addr.family == socket.AFINET:\n out.append({‘interface‘: ifname, ‘family‘: ‘ipv4‘, ‘ip‘: addr.address})\n elif addr.family == socket.AFINET6:\n out.append({‘interface‘: ifname, ‘family‘: ‘ipv6‘, ‘ip‘: addr.address})\n return out\n\n\n def getpublicip(timeouts: float = 4.0) -> strNone:\n # Best-effort: return None rather than failing hard.\n candidates = [\n ‘https://api.ipify.org?format=json‘,\n ‘https://ifconfig.me/all.json‘,\n ]\n\n for url in candidates:\n try:\n req = Request(url, headers={‘User-Agent‘: ‘python-ip-inspector/1.0‘})\n with urlopen(req, timeout=timeouts) as resp:\n payload = resp.read().decode(‘utf-8‘)\n data = json.loads(payload)\n if isinstance(data, dict):\n if ‘ip‘ in data and isinstance(data[‘ip‘], str):\n return data[‘ip‘]\n if ‘ipaddr‘ in data and isinstance(data[‘ipaddr‘], str):\n return data[‘ipaddr‘]\n except Exception:\n continue\n return None\n\n\n def main() -> int:\n parser = argparse.ArgumentParser(description=‘Inspect local and public IP addresses‘)\n parser.addargument(‘–json‘, action=‘storetrue‘, help=‘emit JSON‘)\n parser.addargument(‘–include-interfaces‘, action=‘storetrue‘, help=‘list interface IPs (needs psutil)‘)\n parser.addargument(‘–include-public‘, action=‘storetrue‘, help=‘attempt to fetch public IP over HTTPS‘)\n parser.addargument(‘–allow-link-local-v6‘, action=‘storetrue‘, help=‘do not filter fe80:: addresses‘)\n args = parser.parseargs()\n\n preferredv4 = getpreferredlocalipv4()\n preferredv6 = getpreferredlocalipv6()\n\n interfaces = listinterfaceips() if args.includeinterfaces else []\n if interfaces:\n interfaces = [\n row for row in interfaces\n if isusefulip(row[‘ip‘], allowlinklocalv6=args.allowlinklocalv6)\n ]\n\n publicip = getpublicip() if args.includepublic else None\n if publicip is not None and not isusefulip(publicip, allowlinklocalv6=True):\n # If a service returns something unexpected, hide it.\n publicip = None\n\n result = {\n ‘preferredlocalipv4‘: preferredv4 if isusefulip(preferredv4, allowlinklocalv6=True) else None,\n ‘preferredlocalipv6‘: preferredv6 if isusefulip(preferredv6, allowlinklocalv6=True) else None,\n ‘publicip‘: publicip,\n ‘interfaces‘: interfaces,\n }\n\n if args.json:\n print(json.dumps(result, indent=2, sortkeys=True))\n else:\n print(‘Preferred local IPv4:‘, result[‘preferredlocalipv4‘] or ‘(none)‘)\n print(‘Preferred local IPv6:‘, result[‘preferredlocalipv6‘] or ‘(none)‘)\n if args.includepublic:\n print(‘Public IP:‘, result[‘publicip‘] or ‘(unavailable)‘)\n if args.includeinterfaces:\n if not interfaces:\n print(‘Interface IPs: (none, or psutil not installed)‘)\n else:\n print(‘Interface IPs:‘)\n for row in interfaces:\n print(f" {row[‘interface‘]:<15} {row['family']:<4} {row['ip']}")\n\n return 0\n\n\n if name == ‘main‘:\n raise SystemExit(main())\n\nWhy I like this style for real projects:\n- It separates concerns: preferred local vs public vs enumeration.\n- It is explicit about optional behaviors (public IP fetch, interfaces listing).\n- It avoids crashing on common failure modes (offline, blocked endpoints, missing dependency).\n\nHow I‘d run it in practice:\n- Local debug: python ipinspector.py --include-interfaces\n- Public debug: python ipinspector.py --include-public\n- Tooling pipeline: python ipinspector.py --include-public --include-interfaces --json\n\n## Edge cases I actually hit (and how I handle them)\nThese are the issues that show up in real environments, not just in tidy demos.\n\n### 1) Multi-homed machines: Wi-Fi + Ethernet + VPN\nSymptom: you print one IP and the user insists their laptop is on a different subnet.\n\nWhat‘s happening: the OS may prefer the VPN interface for outbound connections (by design). The "preferred local" IP can change the moment the VPN connects.\n\nWhat I do:\n- For "preferred outbound," I accept the VPN result. That‘s what the OS will do.\n- For "LAN discovery" or "which IP can my phone reach," I print interface lists and let the user pick the Wi-Fi/Ethernet IP.\n\n### 2) IPv6 zone IDs (scope identifiers)\nSymptom: you see an address like fe80::1c2:3dff:fe4a:5b6c%en0.\n\nWhat‘s happening: link-local IPv6 addresses require an interface scope to be usable. Some APIs include that %en0 suffix.\n\nWhat I do:\n- When parsing, strip %... before feeding it into ipaddress.\n- When displaying, keep it, because it‘s often needed to actually use the address.\n\n### 3) Offline machines and sandboxed environments\nSymptom: UDP connect trick throws OSError, and public IP lookup fails.\n\nWhat‘s happening: you might be offline, or outbound traffic might be blocked by policy.\n\nWhat I do:\n- Return a fallback (loopback) for preferred local if I‘m in a best-effort context.\n- Or raise a clear error if the program cannot function without network (depends on the app).\n\n### 4) Containers and "it works on my host" confusion\nSymptom: code prints a container IP, user tries to connect from the host and fails.\n\nWhat‘s happening: the container IP is not reachable from the host in the way they expect.\n\nWhat I do:\n- In container-first apps, print advice rather than a single IP.\n- For example: "If running in Docker, publish the port and use localhost from the host."\n\n### 5) Corporate proxies and "public IP" weirdness\nSymptom: public IP endpoint returns an IP that doesn‘t match what other sites show, or requests fail.\n\nWhat‘s happening: proxies, split tunnels, or security gateways can alter egress.\n\nWhat I do:\n- Treat public IP as informational, not authoritative identity.\n- Provide fallback endpoints and short timeouts.\n- Avoid blocking critical startup on the public IP lookup.\n\n## Common pitfalls (and the corrections that save time)\nIf you skim everything else, these are the mistakes I see most often.\n\n1) Mistake: using gethostbyname(gethostname()) as "my LAN IP"\n- Fix: use UDP connect trick for preferred local, or enumerate interfaces for actual LAN addresses.\n\n2) Mistake: printing a LAN IP but binding to 127.0.0.1\n- Fix: bind to 0.0.0.0 (or ::) when you want reachability, then print the LAN IPs as connection hints.\n\n3) Mistake: calling a public IP endpoint constantly\n- Fix: cache it, or fetch it only when needed.\n\n4) Mistake: assuming IPv4 exists everywhere\n- Fix: support IPv6 or at least fail gracefully. In 2026, IPv6-only or IPv6-preferred networks are not rare.\n\n5) Mistake: assuming one "correct" answer\n- Fix: show multiple results categorized (preferred local v4, preferred local v6, all interface IPs, public). This makes the output self-explanatory.\n\n## A tiny testing approach (so refactors don‘t break networking output)\nIP code feels "too small to test" until it breaks in production. I don‘t fully integration-test routing behavior (that‘s environment-dependent), but I do test the parts that are deterministic:\n- The filtering rules (isusefulip)\n- The parsing of IPv6 zone IDs\n- The JSON shape if I‘m emitting machine-readable output\n\nExample unit tests (no network calls):\n\n import unittest\n\n\n class TestIPHelpers(unittest.TestCase):\n def testisusefulipfiltersloopback(self):\n from ipinspector import isusefulip\n self.assertFalse(isusefulip(‘127.0.0.1‘))\n self.assertFalse(isusefulip(‘::1‘))\n\n def testisusefulipstripszoneid(self):\n from ipinspector import isusefulip\n self.assertFalse(isusefulip(‘fe80::1%en0‘))\n self.assertTrue(isusefulip(‘fe80::1%en0‘, allowlinklocalv6=True))\n\n def testisusefulipfiltersipv4linklocal(self):\n from ipinspector import isusefulip\n self.assertFalse(isusefulip(‘169.254.10.20‘))\n\n\n if name == ‘main‘:\n unittest.main()\n\nThese tests won‘t tell you which interface your laptop picks on Tuesday after a VPN update, but they will prevent the embarrassing regression where a refactor starts returning loopback addresses or failing on zone IDs.\n\n## Troubleshooting: when the output surprises you\nHere are the fast diagnoses I use.\n\n### "It prints 127.0.0.1"\nLikely cause:\n- You‘re using hostname-based resolution, or you fell back because the machine is offline.\n\nFix:\n- Switch to UDP connect trick for preferred local.\n- If you‘re offline, decide whether you should return loopback or raise a clear error.\n\n### "It prints a Docker subnet (172.17.x.x)"\nLikely cause:\n- You‘re running inside a container, and that is the container IP.\n\nFix:\n- Bind to 0.0.0.0 and publish ports; use localhost from the host.\n- If you truly need the host‘s IP from inside the container, that‘s a deployment-level concern, not a generic Python trick.\n\n### "It prints my VPN address"\nLikely cause:\n- Your default route is through the VPN (common and usually correct).\n\nFix:\n- If your intent is outbound source IP, accept it.\n- If your intent is LAN reachability, list interface IPs and pick the Wi-Fi/Ethernet address.\n\n### "Public IP lookup fails"\nLikely cause:\n- No internet, endpoint blocked, corporate TLS inspection issues, DNS issues, or timeout too low.\n\nFix:\n- Add endpoint fallbacks and a slightly larger timeout.\n- Treat public IP as optional in startup paths.\n\n## What I recommend in 2026 (the opinionated version)\nIf you asked me to standardize this across a fleet of Python services and CLIs, here‘s the approach I‘d choose:\n\n- Default local IP (outbound): UDP connect trick for IPv4 and IPv6, with clear naming like preferredlocalipv4.\n- Interface listing (tooling): psutil.netifaddrs() with ipaddress filtering.\n- Public IP: HTTPS JSON endpoint, cached, best-effort, never on the hot path.\n- UX: print multiple categorized answers instead of pretending one IP is always "the" answer.\n\nThat combination stays small, behaves predictably across OSes, and fails in ways that are easy to explain to users (and to your future self).


