NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #1808 most downloaded on PyPI
A Python package to retrieve a client's real IP address.
Last release 3 days ago
01 Oct 2026
Ships unpredictably
gaps range from 8 days to 2.4 years
Nearly every release is documented
notes for 11 of 12 stable releases
Nothing withdrawn
no release was ever pulled
4 years old
12 releases · first in 2023
One column per quarter.
Patch release for the modern engine. The legacy engine is unchanged.
Patch release for the modern engine. The legacy engine is unchanged.
000080 are still accepted.Reported privately by David Gilman. Thank you.
Upgrade with pip install -U python-ipware.
Reported privately by David Gilman.
Harden (modern engine only; legacy is unchanged):
99999, instead of calling int() on an unbounded digit string. A client-controlled header of about 4.3 KB could otherwise make get_client_ip() raise ValueError (Python's int string-digit limit). Zero-padded ports such as 000080 are still accepted.The first 4.x release on PyPI. 4.0.0 was tagged internally but never published, so this release carries everything since 3.0.0: a pluggable algorithm
The first 4.x release on PyPI. 4.0.0 was tagged internally but never published, so this
release carries everything since 3.0.0: a pluggable algorithm router with a new default
engine, a better best-match for the client IP, tighter trusted-proxy matching, and a much
larger test suite. Existing code keeps working: from python_ipware import IpWare is
unchanged.
modern. For normal, well-formed headers it returns the sameIpWare(algorithm="legacy") runs the frozen v3 algorithm.proxy_list is stricter. A complete IP entry now matches exactly, not as a prefix. If you"1.2.3.4" also matching 1.2.3.45, use a CIDR or a prefix ending in ..ValueError when IpWare(...) is created instead of silentlyproxy_list entry, a bare string instead of a list, a non-IPproxy_count.IpWare(algorithm="auto" | "modern" | "legacy"). auto (the default) resolves to modern.
legacy keeps the v3 algorithm byte for byte, validated by the original v3 test suite.
0.0.0.0/8,224.0.0.1 as the client.)10.0.0.1, 177.139.233.139 now yields 177.139.233.139. With leftmost=False the chain isproxy_count orproxy_list when you must identify private (intranet / VPN) clients.proxy_count / proxy_list, the client position is fixed exactly as in 3.x, andtrusted_route is now True for private clients behind trusted proxies too.Forwarded is parsed by its for= value, quote-aware, including for="[2001:db8::1]:4711".::ffff:1.2.3.4) and NAT64 (64:ff9b::1.2.3.4) addresses unwrap to plain IPv4.[::1, [::1]junk, 1.2.3.4:abc, 1.2.3.4:70000.- and _ equivalent), so lowercase keys such as AWSx_forwarded_for cannot shadow the proxy's x-forwarded-for.proxy_list entries: a CIDR network (IPv4 or IPv6, by membership), a complete IP"10.1" matches 10.1.x.x, not10.100.x.x). IPv6 entries ignore case and leading zeros.::ffff:10.0.0.0/104) match the unwrapped IPv4 hops.Added, in order of arrival: True-Client-IP, Fastly-Client-IP, Fly-Client-IP, App Engine
X-AppEngine-User-IP, Azure X-Client-IP, Azure Front Door X-Azure-ClientIP, DigitalOcean
DO-Connecting-IP, Envoy X-Envoy-External-Address. Headers released earlier never move; new
ones sit just above REMOTE_ADDR, so an upgrade never lets a new header outrank one that already
resolved your requests.
pyproject.toml with the Hatchling backend; the version lives in __version__.py.v* tags via PyPI trusted publishing.182 tests. The modern engine has 100% line and branch coverage, checked by an exhaustive matrix
against an independent reference model, a never-worse-than-legacy differential over the whole
matrix, formatting-invariance and fuzz runs, and mutation checks. The original v3 suite passes on
both engines.
@griffi-gh (#26, CIDR proxy entries), @mdalp (#23, Fly.io), @iloveitaly (#24, #25).
Full details: CHANGELOG.md.
Best match (modern engine only; legacy is unchanged). Results can differ from 4.0.0 on well-formed input,
always toward a better address; use algorithm="legacy" for exact v3 results:
0.0.0.0, ::),
multicast, broadcast, and reserved addresses are never returned. Python reports multicast as
is_global, so v3 could return 224.0.0.1 as the client.proxy_count / proxy_list, the first public hop of a chain wins, not only the first hop:
10.0.0.1, 177.139.233.139 now yields 177.139.233.139. With leftmost=False the scan runs from the
right. With proxy settings, the client position is fixed exactly as before. Note that the public hop
may be an upstream proxy: if you must identify private (intranet / VPN) clients, set proxy_count or
proxy_list.64:ff9b::a.b.c.d, RFC 6052) are unwrapped to the embedded IPv4
client, like IPv4-mapped addresses. v3 returned the IPv6 form.trusted_route is True for any address resolved through a matching proxy config, including private
clients; v3 reported False for them.Enhance (modern engine only; legacy is unchanged):
Forwarded elements by their for= value, including quoted, bracketed IPv6 with a port.
Previously Forwarded never produced an IP, so when it is present it can now resolve at its existing
precedence slot, which is above the CDN headers. Like X-Forwarded-For, a client can send it; behind a
CDN, pass an explicit precedence naming that CDN's header. Obfuscated hops (for=unknown, for=_hidden) count as invalid tokens.REMOTE_ADDR, so none outranks a header
that resolved requests before: Azure Front Door X-Azure-ClientIP, DigitalOcean DO-Connecting-IP,
Envoy/Istio X-Envoy-External-Address, plus the missing HTTP_X_CLIENT_IP and raw X-AppEngine-User-IP
forms of headers already on the list.- and _ equivalent), so lowercase keys such as AWS Lambda's
work. Exact keys still take priority. When several spellings fold to the same header, the dash spelling
wins, whatever the dict order, so a client-sent x_forwarded_for cannot shadow the proxy's
x-forwarded-for. Dash spellings that disagree are treated as absent.Harden (modern engine only):
[::1), text after a bracket
([::1]junk), and non-numeric, empty, or out-of-range ports (1.2.3.4:abc, 1.2.3.4:70000). Note v3
accepted 1.2.3.4:abc as 1.2.3.4.None, bytes) are skipped instead of raising AttributeError.proxy_list entries are stripped of whitespace. An empty entry now raises ValueError: it used to match
every address and mark any spoofed chain as trusted."1.2.3.4" also trusted 1.2.3.45, letting that host forge the client IP. Prefixes match on whole
octets or groups ("10.1" matches 10.1.x.x, not 10.100.x.x). IPv6 entries are case- and
zero-insensitive. An entry ending in : stays a prefix, so "2001:db8::" behaves as in 4.0.0.
IPv4-mapped and NAT64 CIDR entries (::ffff:10.0.0.0/104) match the unwrapped IPv4 hops.ValueError at construction instead of misbehaving silently: proxy_list
or precedence passed as a bare string (each character became an entry), a non-IP proxy_list entry
such as "foo", or a proxy_count that is a bool, a float, or a string. precedence and proxy_list
are copied, so later changes to the caller's lists have no effect.meta that is not a mapping raises TypeError with a clear message. Non-string keys are skipped.Forwarded parsing is quote-aware: a , or ; inside a quoted value no longer splits a hop,
so ext="x,8.8.8.8" cannot smuggle in a fake address or a fake proxy hop.0.0.0.0/8 is never returned. Deprecated site-local fec0::/10 ranks as
private, since Python reports it as global. RFC 8215 local-use NAT64 64:ff9b:1::/48 ranks as private,
since Python reports it as reserved.is_valid_ip helper was removed from python_ipware.modern.parsers. It was never exported.CI:
actions/upload-artifact to v7 and actions/download-artifact to v8, which run on Node 24.version 3 major
version 3 major (#21)
Fix:
Enhance: - AI assisted clean up
Enhance:
Added proxy_count=0 as an option (@FraKraBa)
Enhance:
proxy_count=0 as an option (@FraKraBa)update readme
Enhance:
HTTP_CF_CONNECTING_IP to list of known ip headers (Adam M.)Added logger name
Added logger name (#15)
HTTP_VIA removal, py 3.12
HTTP_VIA removal, py 3.12
Issue:
HTTP_VIA header support (unreliable IP information) (@yourcelf)Enhance:
Rename ipware to python_ipware to avoid conflict with django-ipware
Rename ipware to python_ipware to avoid conflict with django-ipware
django-ipware package.ipware to python_ipware in the python-ipware package.
from ipware import IpWarefrom python_ipware import IpWare- Enhance: Readme updates
Nothing published for this version
Features: - Initial Release
Features:
Your coding agent can read these notes before it upgrades. Set up the MCP server →