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.

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:

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

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.

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

Shell
\$ curl -sS ifconfig.me/ip
220.14.9.37
Copied!

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:

Python

Copied!

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:

Python
>>> IPv4Address(3691907365)  # From an int

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

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:

Python
3691907365
b'\xdc\x0e\t%'
Copied!

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:

Python
4035855712965130587

>>> num_connections = {
... }
Copied!

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

Python
True

... )
...     print(a)
...
8.240.12.2
100.201.0.4
220.14.9.37
Copied!

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:

Python
>>> net = IPv4Network("192.4.2.0/24")
256
Copied!

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:

Python
>>> net.prefixlen
24
Copied!

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:

Python
Copied!

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:

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

True
False
Copied!

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:

Python
Copied!

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:

Python
>>> net.prefixlen
24
IPv4Address('255.255.255.0')  # 11111111 11111111 11111111 00000000
Copied!

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

Python
>>> IPv4Network("192.4.2.0/255.255.255.0")
IPv4Network('192.4.2.0/24')
Copied!

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:

Python
Copied!

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

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:

Python
>>> net = IPv4Network("100.64.0.0/10")
4194304
Copied!

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:

Python
>>> net = IPv4Network("192.4.2.0/28")
...
192.4.2.0
192.4.2.1
192.4.2.2
...
192.4.2.13
192.4.2.14
192.4.2.15
Copied!

Similarly, net.hosts() returns a generator that will yield the addresses shown above, excluding the network and broadcast addresses:

Python
>>> h = net.hosts()
>>> type(h)
<class 'generator'>
>>> next(h)
>>> next(h)
Copied!

In the next section, you’ll dive in to a concept closely related to networks: the subnet.

### Subnets

A subnet is a subdivision of an IP network:

Python
>>> small_net = IPv4Network("192.0.2.0/28")
>>> big_net = IPv4Network("192.0.0.0/16")
>>> small_net.subnet_of(big_net)
True
>>> big_net.supernet_of(small_net)
True
Copied!

Above, small_net contains only 16 addresses, which is sufficient for you and a few cubicles around you. Conversely, big_net contains 65,536 addresses.

A common way to achieve subnetting is to take a network and increase its prefix length by 1. Let’s take this example from Wikipedia:

This example starts with a /24 network:

Python
net = IPv4Network("200.100.10.0/24")
Copied!

Subnetting by increasing the prefix length from 24 to 25 involves shifting bits around to break up the network into smaller parts. This is a bit mathematically hairy. Luckily, IPv4Network makes it a cinch because .subnets() returns an iterator over the subnets:

Python
>>> for sn in net.subnets():
...     print(sn)
...
200.100.10.0/25
200.100.10.128/25
Copied!

You can also tell .subnets() what the new prefix should be. A higher prefix means more and smaller subnets:

Python
>>> for sn in net.subnets(new_prefix=28):
...     print(sn)
...
200.100.10.0/28
200.100.10.16/28
200.100.10.32/28
...
200.100.10.208/28
200.100.10.224/28
200.100.10.240/28
Copied!

Besides addresses and networks, there’s a third core part of the ipaddress module that you’ll see next.

### Host Interfaces

Last but certainly not least, Python’s ipaddress module exports an IPv4Interface class for representing a host interface. A host interface is a way to describe, in a single compact form, both a host IP address and a network that it sits in:

Python

>>> ifc = IPv4Interface("192.168.1.6/24")
>>> ifc.ip  # The host IP address
>>> ifc.network  # Network in which the host IP resides
IPv4Network('192.168.1.0/24')
Copied!

Above, 192.168.1.6/24 means “the IP address 192.168.1.6 in the network 192.168.1.0/24.”

Put differently, an IP address alone doesn’t tell you which network(s) that address sits in, and a network address is a group of IP addresses rather than a single one. The IPv4Interface gives you a way of simultaneously expressing, through CIDR notation, a single host IP address and its network.

Now that you know about both IP addresses and networks at a high level, it’s also important to know that not all IP addresses are created equal—some are special.

The Internet Assigned Numbers Authority (IANA), in tandem with the Internet Engineering Task Force (IETF), oversees the allocation of different address ranges. IANA’s IPv4 Special-Purpose Address Registry is a very important table dictating that certain address ranges should have special meanings.

A common example is that of a private address. A private IP address is used for internal communication between devices on a network that doesn’t require connectivity to the public Internet. The following ranges are reserved for private use:

10.0.0.0/8 16,777,216 10.0.0.0 10.255.255.255
172.16.0.0/12 1,048,576 172.16.0.0 172.31.255.255
192.168.0.0/16 65,536 192.168.0.0 192.168.255.255

A randomly chosen example is 10.243.156.214. So, how do you know that this address is private? You can confirm that it falls in the 10.0.0.0/8 range:

Python
True
Copied!

A second special address type is a link-local address, which is one reachable only from within a given subnet. An example is the Amazon Time Sync Service, which is available for AWS EC2 instances at the link-local IP 169.254.169.123. If your EC2 instance sits in a virtual private cloud (VPC), then you don’t need an Internet connection to tell your instance what time it is. The block 169.254.0.0/16 is reserved for link-local addresses:

Python
True
Copied!

Above, you can see that one way to confirm that 10.243.156.214 is a private-use address is to test that it sits in the 10.0.0.0/8 range. But Python’s ipaddress module also provides a set of properties for testing whether an address is a special type:

Python
True
True

>>> [i for i in dir(IPv4Address) if i.startswith("is_")]  # "is_X" properties
['is_global',
'is_loopback',
'is_multicast',
'is_private',
'is_reserved',
'is_unspecified']
Copied!

One thing to note about .is_private, though, is that it uses a broader definition of private network than the three IANA ranges shown in the table above. Python’s ipaddress module also lumps in other addresses that are allocated for private networks:

This is not an exhaustive list, but it covers the most common cases.

## The Python ipaddress Module Under the Hood

In addition to its documented API, the CPython source code for the ipaddress module and its IPv4Address class gives some great insights into how you can use a pattern called composition to lend your own code an idiomatic API.

### Composition’s Core Role

The ipaddress module takes advantage of an object-oriented pattern called composition. Its IPv4Address class is a composite that wraps a plain Python integer. IP addresses are fundamentally integers, after all.

Each IPv4Address instance has a quasi-private ._ip attribute that is itself an int. Many of the other properties and methods of the class are driven by the value of this attribute:

Python
3691907365
Copied!

If you asked the CPython developers about this, then they might tell you that ._ip is an implementation detail. While nothing is truly private in Python, the leading underscore denotes that ._ip is quasi-private, not part of the public ipaddress API, and subject to change without notice. That’s why it’s more stable to extract the underlying integer with int(addr).

With all that said, though, it’s the underlying ._ip that gives the IPv4Address and IPv4Network classes their magic.

You can demonstrate the power of the underlying ._ip integer by extending the IPv4 address class:

Python

raise NotImplementedError
return self.__class__(int(self) & int(other))
Copied!

Adding .__and__() lets you use the binary AND (&) operator. Now you can directly apply a netmask to a host IP:

Python
>>> mask = MyIPv4("255.192.0.0")  # A /10 prefix

MyIPv4('100.64.0.0')

>>> addr & 0xffc00000  # Hex literal for 255.192.0.0
MyIPv4('100.64.0.0')
Copied!

Above, .__and__() allows you to use either another IPv4Address or an int directly as the mask. Because MyIPv4 is a subclass of IPv4Address, the isinstance() check will return True in that case.

Python
1import re
3
5    @property
6    def binary_repr(self, sep=".") -> str:
7        """Represent IPv4 as 4 blocks of 8 bits."""
8        return sep.join(f"{i:08b}" for i in self.packed)
9
10    @classmethod
11    def from_binary_repr(cls, binary_repr: str):
12        """Construct IPv4 from binary representation."""
13        # Remove anything that's not a 0 or 1
14        i = int(re.sub(r"[^01]", "", binary_repr), 2)
15        return cls(i)
Copied!

In .binary_repr (line 8), using .packed transforms the IP address into an array of bytes that is then formatted as the string representation of its binary form.

In .from_binary_repr, the call to int(re.sub(r"[^01]", "", binary_repr), 2) on line 14 has two parts:

1. It removes anything besides 0s and 1s from the input string.
2. It parses the result, assuming base 2, with int(<string>, 2).

Using .binary_repr() and .from_binary_repr() allows you to convert to and construct from a str of 1s and 0s in binary notation:

Python
>>> MyIPv4("220.14.9.37").binary_repr
'11011100.00001110.00001001.00100101'
>>> MyIPv4("255.255.0.0").binary_repr  # A /16 netmask
'11111111.11111111.00000000.00000000'

>>> MyIPv4.from_binary_repr("11011100 00001110 00001001 00100101")
MyIPv4('220.14.9.37')
Copied!

These are just a few ways demonstrating how taking advantage of the IP-as-integer pattern can help you extend the functionality of IPv4Address with a small amount of additional code.

## Conclusion

In this tutorial, you saw how Python’s ipaddress module can allow you to work with IP addresses and networks using common Python constructs.

Here are some important points that you can take away:

• An IP address is fundamentally an integer, and this underlies both how you can do manual arithmetic with addresses and how the Python classes from ipaddress are designed using composition.
• The ipaddress module uses composition, and you can extend that functionality as needed for added behavior.

As always, if you’d like to dive deeper, then reading the module source is a great way to do that.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Brad is a software engineer and a member of the Real Python Tutorial Team.

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

What Do You Think?