Learn IP Address Concepts With Python's ipaddress Module

Learn IP Address Concepts With Python's ipaddress Module

by Brad Solomon Updated Reading time estimate 18m intermediate stdlib

Python’s ipaddress module is an underappreciated gem from the Python standard library. You don’t have to be a full-blown network engineer to have been exposed to IP addresses in the wild. IP addresses and networks are ubiquitous in software development and infrastructure. They underpin how computers, well, address each other.

Learning through doing is an effective way to master IP addresses. The ipaddress module allows you to do just that by viewing and manipulating IP addresses as Python objects. In this tutorial, you’ll get a better grasp of IP addresses by using some of the features of Python’s ipaddress module.

In this tutorial, you’ll learn:

  • How IP addresses work, both in theory and in Python code
  • How IP networks represent groups of IP addresses and how you can inspect relationships between the two
  • How Python’s ipaddress module cleverly uses a classic design pattern to allow you to do more with less

To follow along, you just need Python 3.3 or higher since ipaddress was added to the Python standard library in that version. The examples in this tutorial were generated using Python 3.8.

IP Addresses in Theory and Practice

If you remember only one concept about IP addresses, then remember this: an IP address is an integer. This piece of information will help you better understand both how IP addresses function and how they can be represented as Python objects.

Before you jump into any Python code, it can be helpful to see this concept fleshed out mathematically. If you’re here just for some examples of how to use the ipaddress module, then you can skip down to the next section, on using the module itself.

Mechanics of IP Addresses

You saw above that an IP address boils down to an integer. A fuller definition is that an IPv4 address is a 32-bit integer used to represent a host on a network. The term host is sometimes used synonymously with an address.

It follows that there are 232 possible IPv4 addresses, from 0 to 4,294,967,295 (where the upper bound is 232 - 1). But this is a tutorial for human beings, not robots. No one wants to ping the IP address 0xdc0e0925.

The more common way to express an IPv4 address is using quad-dotted notation, which consists of four dot-separated decimal integers:

220.14.9.37

It’s not immediately obvious what underlying integer the address 220.14.9.37 represents, though. Formulaically, you can break the IP address 220.14.9.37 into its four octet components:

Language: Python
>>> (
...     220 * (256 ** 3) +
...      14 * (256 ** 2) +
...       9 * (256 ** 1) +
...      37 * (256 ** 0)
... )
3691907365

As shown above, the address 220.14.9.37 represents the integer 3,691,907,365. Each octet is a byte, or a number from 0 to 255. Given this, you can infer that the maximum IPv4 address is 255.255.255.255 (or FF.FF.FF.FF in hex notation), while the minimum is 0.0.0.0.

Next, you’ll see how Python’s ipaddress module does this calculation for you, allowing you to work with the human-readable form and let the address arithmetic occur out of sight.

The Python ipaddress Module

To follow along, you can fetch your computer’s external IP address to work with at the command line:

Language: Shell
$ curl -sS ifconfig.me/ip
220.14.9.37

This requests your IP address from the site ifconfig.me, which can be used to show an array of details about your connection and network.

Now open up a Python REPL. You can use the IPv4Address class to build a Python object that encapsulates an address:

Language: Python
>>> from ipaddress import IPv4Address

>>> addr = IPv4Address("220.14.9.37")
>>> addr
IPv4Address('220.14.9.37')

Passing a str such as "220.14.9.37" to the IPv4Address constructor is the most common approach. However, the class can also accept other types:

Language: Python
>>> IPv4Address(3691907365)  # From an int
IPv4Address('220.14.9.37')

>>> IPv4Address(b"\xdc\x0e\t%")  # From bytes (packed form)
IPv4Address('220.14.9.37')

While constructing from a human-readable str is probably the more common way, you might see bytes input if you’re working with something like TCP packet data.

The conversions above are possible in the other direction as well:

Language: Python
>>> int(addr)
3691907365
>>> addr.packed
b'\xdc\x0e\t%'

In addition to allowing round-trip input and output to different Python types, instances of IPv4Address are also hashable. This means you can use them as keys in a mapping data type such as a dictionary:

Language: Python
>>> hash(IPv4Address("220.14.9.37"))
4035855712965130587

>>> num_connections = {
...     IPv4Address("220.14.9.37"): 2,
...     IPv4Address("100.201.0.4"): 16,
...     IPv4Address("8.240.12.2"): 4,
... }

On top of that, IPv4Address also implements methods that allow for comparisons using the underlying integer:

Language: Python
>>> IPv4Address("220.14.9.37") > IPv4Address("8.240.12.2")
True

>>> addrs = (
...     IPv4Address("220.14.9.37"),
...     IPv4Address("8.240.12.2"),
...     IPv4Address("100.201.0.4"),
... )
>>> for a in sorted(addrs):
...     print(a)
...
8.240.12.2
100.201.0.4
220.14.9.37

You can use any of the standard comparison operators to compare the integer values of address objects.

As you’ve seen above, the constructor itself for IPv4Address is short and sweet. It’s when you start lumping addresses into groups, or networks, that things become more interesting.

IP Networks and Interfaces

A network is a group of IP addresses. Networks are described and displayed as contiguous ranges of addresses. For example, a network may be composed of the addresses 192.4.2.0 through 192.4.2.255, a network containing 256 addresses.

You could recognize a network by its upper and lower IP addresses, but how can you display this with a more succinct convention? That’s where CIDR notation comes in.

CIDR Notation

A network is defined using a network address plus a prefix in Classless Inter-Domain Routing (CIDR) notation:

Language: Python
>>> from ipaddress import IPv4Network
>>> net = IPv4Network("192.4.2.0/24")
>>> net.num_addresses
256

CIDR notation represents a network as <network_address>/<prefix>. The routing prefix (or prefix length, or just prefix), which is 24 in this case, is the count of leading bits used to answer questions such as whether a certain address is part of a network or how many addresses reside in a network. (Here leading bits refers to the first N bits counting from the left of the integer in binary.)

You can find the routing prefix with the .prefixlen property:

Language: Python
>>> net.prefixlen
24

Let’s jump right into an example. Is the address 192.4.2.12 in the network 192.4.2.0/24? The answer in this case is yes, because the leading 24 bits of 192.4.2.12 are the first three octets (192.4.2). With a /24 prefix, you can simply chop off the last octet and see that the 192.4.2.xxx parts match.

Pictured differently, the /24 prefix translates to a netmask that, as its name implies, is used to mask bits in the addresses being compared:

Language: Python
>>> net.netmask
IPv4Address('255.255.255.0')

You compare leading bits to determine whether an address is part of a network. If the leading bits match, then the address is part of the network:

11000000 00000100 00000010 00001100  # 192.4.2.12  # Host IP address
11000000 00000100 00000010 00000000  # 192.4.2.0   # Network address
                          |
                          ^ 24th bit (stop here!)
|_________________________|
            |
      These bits match

Above, the final 8 bits of 192.4.2.12 are masked (with 0) and are ignored in the comparison. Once again, Python’s ipaddress saves you the mathematical gymnastics and supports idiomatic membership testing:

Language: Python
>>> net = IPv4Network("192.4.2.0/24")

>>> IPv4Address("192.4.2.12") in net
True
>>> IPv4Address("192.4.20.2") in net
False

This is made possible by the treasure that is operator overloading, by which IPv4Network defines __contains__() to allow membership testing using the in operator.

In the CIDR notation 192.4.2.0/24, the 192.4.2.0 part is the network address, which is used to identify the network:

Language: Python
>>> net.network_address
IPv4Address('192.4.2.0')

As you saw above, the network address 192.4.2.0 can be seen as the expected result when a mask is applied to a host IP address:

11000000 00000100 00000010 00001100  # Host IP address
11111111 11111111 11111111 00000000  # Netmask, 255.255.255.0 or /24
11000000 00000100 00000010 00000000  # Result (compared to network address)

When you think about it this way, you can see how the /24 prefix actually translates into a true IPv4Address:

Language: Python
>>> net.prefixlen
24
>>> net.netmask
IPv4Address('255.255.255.0')  # 11111111 11111111 11111111 00000000

In fact, if it strikes your fancy, you can construct an IPv4Network directly from two addresses:

Language: Python
>>> IPv4Network("192.4.2.0/255.255.255.0")
IPv4Network('192.4.2.0/24')

Above, 192.4.2.0 is the network address while 255.255.255.0 is the netmask.

At the other end of the spectrum in a network is its final address, or broadcast address, which is a single address that can be used to communicate to all the hosts on its network:

Language: Python
>>> net.broadcast_address
IPv4Address('192.4.2.255')

There’s one more point worth mentioning about the netmask. You’ll most often see prefix lengths that are multiples of 8:

Prefix Length Number of Addresses Netmask
8 16,777,216 255.0.0.0
16 65,536 255.255.0.0
24 256 255.255.255.0
32 1 255.255.255.255

However, any integer between 0 and 32 is valid, though less common:

Language: Python
>>> net = IPv4Network("100.64.0.0/10")
>>> net.num_addresses
4194304
>>> net.netmask
IPv4Address('255.192.0.0')

In this section, you saw how to construct an IPv4Network instance and test whether a certain IP address sits within it. In the next section, you’ll learn how to loop over the addresses within a network.

Looping Through Networks

The IPv4Network class supports iteration, meaning that you can iterate over its individual addresses in a for loop:

Language: Python
>>> net = IPv4Network("192.4.2.0/28")