# IPy: a Python class for IPv4 and IPv6 addresses with a Perl NET::IP shape

> IPy is a Python module whose IP class parses about a dozen notations for IPv4 and IPv6 addresses and networks, converts between them, and prints them in whichever prefix or netmask form you ask for. It was inspired by RIPE's Perl NET::IP without sharing its implementation, and its IPSet class aggregates overlapping ranges into parent networks.

**autocracy/python-ipy** — IPy are a Python class and tools for handling of IPv4 and IPv6 addresses and networks. It is similar to Net::IP Perl module.

- Repository: https://github.com/autocracy/python-ipy
- Stars: 569 · Forks: 124
- Language: Python
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/autocracy-python-ipy

## One class, a dozen notations, and the Perl module it came from

IPy is a class and tools for handling of IPv4 and IPv6 addresses and networks, and the readme states plainly what it is modelled on. The IP class allows a comfortable parsing and handling for most notations in use for IPv4 and IPv6 addresses and networks. It was greatly inspired by RIPE's Perl module NET::IP's interface but doesn't share the implementation. It doesn't share non-CIDR netmasks, so funky stuff like a netmask of 0xffffff0f can't be done here.

That last sentence is the boundary, and it is worth reading before you port anything. NET::IP can express netmasks that are not contiguous CIDR prefixes, and IPy deliberately does not support them. If your inputs are always proper CIDR, the limitation never surfaces. If they are not, you need a different library.

The demonstration in the readme shows the shape of the API. An IP object is iterable, and iterating a network yields each address:

```
    >>> from IPy import IP
    >>> ip = IP('127.0.0.0/30')
    >>> for x in ip:
    ...  print(x)
    ...
    127.0.0.0
    127.0.0.1
    127.0.0.2
    127.0.0.3
```

And the same network can be written in hexadecimal form and still compare equal, which is the sort of thing that makes the class convenient in a script that receives addresses from several sources:

```
    >>> ip2 = IP('0x7f000000/30')
    >>> ip == ip2
    1
```

The comparison returning 1 rather than True is a detail of the era, and the rest of the API is older still. The readme notes that IPy 1.2 works on Python 3.5 and later, and that you should use IPy 1.01 for Python 2.

## Parsing the notations people actually paste in

The format support section is the practical centre of the module, and it separates IPv4 from IPv6 in the way the documentation claims.

It can detect about a dozen different ways of expressing IP addresses and networks, parse them and distinguish between IPv4 and IPv6 addresses. The version() method tells you which one you have, returning 4 or 6.

For IPv4 the accepted forms include the integer value, the hex string and the dotted quad, plus a bare integer that is interpreted as a network address:

```
    >>> print(IP(0x7f000001))
    127.0.0.1
    >>> print(IP('0x7f000001'))
    127.0.0.1
    >>> print(IP('127.0.0.1'))
    127.0.0.1
    >>> print(IP('10'))
    10.0.0.0
```

For IPv6, the canonical form is compressed with lower case hex, and the mixed notation with an embedded IPv4 tail is understood and converted:

```
    >>> print(IP('1080:0:0:0:8:800:200C:417A'))
    1080::8:800:200c:417a
    >>> print(IP('::1'))
    ::1
    >>> print(IP('::13.1.68.3'))
    ::d01:4403
```

Networks can be given in three ways and all three mean the same thing. A prefix length, a dotted netmask, or an explicit address range:

```
    >>> print(IP('127.0.0.0/8'))
    127.0.0.0/8
    >>> print(IP('127.0.0.0/255.0.0.0'))
    127.0.0.0/8
    >>> print(IP('127.0.0.0-127.255.255.255'))
    127.0.0.0/8
```

The last of those is the interesting one. A range expressed as two addresses is normalised to the smallest enclosing network, which means the library will quietly widen a range that was not aligned to a prefix boundary. That is the correct behaviour for a network library, but it is also a rounding that a caller needs to know about.

## String conversion is a parameter, not a fixed format

The way an address prints is one of the more surprising parts of the API, and the readme spends a section on it.

Nearly all class methods which return a string have an optional parameter called wantprefixlen which controls if the prefixlen or netmask is printed. Per default the prefixlen is always shown if the network contains more than one address, and the parameter selects the format:

```
    wantprefixlen == 0 / None     don't return anything   1.2.3.0
    wantprefixlen == 1            /prefix                 1.2.3.0/24
    wantprefixlen == 2            /netmask                1.2.3.0/255.255.255.0
    wantprefixlen == 3            -lastip                 1.2.3.0-1.2.3.255
```

The fourth format, the one that prints the last address of the range, exists because it is how ranges were written before CIDR notation was common, and it is what you need when talking to a system that still expects a start and end address.

The strNormal method is the direct way to ask for a specific format, and the four variants of it show the same network rendered four ways:

```
    >>> IP('10.0.0.0/32').strNormal()
    '10.0.0.0'
    >>> IP('10.0.0.0/24').strNormal(0)
    '10.0.0.0'
    >>> IP('10.0.0.0/24').strNormal(3)
    '10.0.0.0-10.0.0.255'
```

The defaults are class members rather than function arguments, and they are writable per object. The readme shows that setting NoPrefixForSingleIp to None on an instance suppresses the prefix for a single address, and setting WantPrefixLen to 3 changes the format for that instance only:

```
    >>> ip = IP('10.0.0.0')
    >>> print(ip)
    10.0.0.0
    >>> ip.NoPrefixForSingleIp = None
    >>> print(ip)
    10.0.0.0/32
    >>> ip.WantPrefixLen = 3
    >>> print(ip)
    10.0.0.0-10.0.0.0
```

Mutable class-level defaults that any instance can change is a design that will surprise a reader of modern Python, and it is another marker of the age of the library.

## IPSet aggregates ranges the way routing tables do

The second class is IPSet, and it exists because arithmetic on IP objects alone cannot express the general case.

The readme puts it plainly. Simple addition of neighboring netblocks that can be aggregated will yield a parent network of both, but more complex range mapping and aggregation requires the IPSet class which will hold any number of unique address ranges and will aggregate overlapping ranges.

So subtracting one network from another gives a set, not a single network:

```
    >>> from IPy import IP, IPSet
    >>> IP('10.0.0.0/22') - IP('10.0.2.0/24')
    IPSet([IP('10.0.0.0/23'), IP('10.0.3.0/24')])
    >>> IPSet([IP('10.0.0.0/23'), IP('10.0.3.0/24'), IP('10.0.2.0/24')])
    IPSet([IP('10.0.0.0/22')])
```

The second line is the whole point. Three prefixes that are individually unmergeable collapse into a single parent network when the set is constructed, and the set keeps the aggregation rather than pretending the input was already tidy.

A set can be added to and discarded from, and discarding a single address from a range splits that range, which is what you would expect from a set of ranges:

```
    >>> s = IPSet([IP('10.0.0.0/22')])
    >>> s.add(IP('192.168.1.0/29'))
    >>> s.discard(IP('192.168.1.2'))
    >>> s
    IPSet([IP('10.0.0.0/22'), IP('192.168.1.0/31'), IP('192.168.1.3'), IP('192.168.1.4/30')])
```

Two set operations are documented explicitly. The isdisjoint method tells you whether two sets share any address, and the ampersand operator intersects them:

```
    >>> s & IPSet([IP('10.0.0.0/8')])
    IPSet([IP('10.0.0.0/22')])
```

The isdisjoint method is the one to reach for when you are checking whether two allocations overlap, which is the kind of question an address management script asks constantly.

## Two documented edge cases and an old release process

The compatibility section is short and contains the two things most likely to bite a caller.

The first is the errata. When using IPv6 addresses, it is best to compare using IP().len() instead of len(IP), because addresses with an integer value greater than 64 bits can break the second method, with a link to a Stack Overflow question for the details. The reason is structural: an IPv6 address does not fit in a machine integer on a 64-bit build, so calling len() on the object goes through a path that truncates.

The second is about the test suite rather than the code. Fuzz testing for IPSet will throw spurious errors when the IPSet module combines two smaller prefixes into a larger prefix that matches the random prefix tested against. In other words, a failure during fuzzing does not always mean a real defect, and knowing that saves a wasted investigation.

On the licence, the readme says the module is under BSD license and points to the COPYING file, while no standard licence identifier appears in the repository settings. The setup file classifiers list the BSD License, so the intent is clear even if the identifier is not machine readable.

The release process is written out as comments at the top of setup.py, and it is a snapshot of how Python packages were published in that era. You set the version in three places, in IPy/__init__.py, in setup.py and in README.rst, then run the unit tests with make, then set the release date in the ChangeLog, commit, tag as IPy-x.y, push, and upload the sdist to the index. After the release, the version is bumped to n+1 in the two files and a new empty changelog section is added.

Setting the same version in three files is exactly the kind of thing a modern build backend removes, and it is a fair signal of how far back the tooling here sits. The Makefile reflects the same period, with the test target invoking Python 3.11 explicitly and the Python 2 invocations commented out.

## Conclusion

IPy suits a Python codebase that needs to accept addresses in whatever form a user or a log file supplies, and that wants one object covering IPv4 and IPv6 rather than two libraries. It does not suit a new project, because the standard library has ipaddress since Python 3.3 and the ecosystem has moved on, and the readme pins the tested baseline at Python 3.5 with IPy 1.01 as the Python 2 option. Before reaching for it, check whether the ipaddress module already covers your case, and if you do use IPy, read the two documented gotchas, the IPv6 length comparison and the IPSet fuzz testing behaviour, before relying on it in a security relevant path.

## FAQ

### What is IPy?

It is a Python class and tools for handling IPv4 and IPv6 addresses and networks. The IP class parses about a dozen notations for addresses and networks and distinguishes IPv4 from IPv6, and it was greatly inspired by RIPE's Perl NET::IP module without sharing its implementation.

### Which address formats can IPy parse?

For IPv4 it accepts an integer, a hexadecimal string, a dotted quad and a bare integer treated as a network address. For IPv6 it accepts the full and compressed forms and the mixed notation with an embedded IPv4 tail. Networks can be given as a prefix length, a dotted netmask or an explicit address range, and all three normalise to the same network.

### What does the IPSet class do that IP alone cannot?

IPSet holds any number of unique address ranges and aggregates overlapping ranges into parent networks, so three separately given prefixes can collapse into one network. It supports adding, discarding, an isdisjoint method and intersection with the ampersand operator.

### What are the known pitfalls in IPy?

The readme notes that when using IPv6 addresses it is best to compare using IP().len() rather than len(IP), because addresses with an integer value over 64 bits can break the second method. It also records that fuzz testing IPSet throws spurious errors when two smaller prefixes are combined into a larger one matching the random prefix tested.

## Sources

- [autocracy/python-ipy on GitHub](https://github.com/autocracy/python-ipy)
- [Issues](https://github.com/autocracy/python-ipy/issues)
- [README](https://github.com/autocracy/python-ipy/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/autocracy-python-ipy
