Copied!
Happy Pythoning!

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.

### The Python `ipaddress` Module

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
``````>>> from ipaddress import IPv4Address

``````
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
``````>>> int(addr)
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
``````>>> hash(IPv4Address("220.14.9.37"))
4035855712965130587

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

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

Python
``````>>> IPv4Address("220.14.9.37") > IPv4Address("8.240.12.2")
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
``````>>> from ipaddress import IPv4Network
>>> 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
``````>>> net.netmask
``````
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
``````>>> net.network_address
``````
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!

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:

Python
``````>>> net.broadcast_address
``````
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
``````>>> from ipaddress import IPv4Interface

>>> 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
``````>>> IPv4Address("10.243.156.214") in IPv4Network("10.0.0.0/8")
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
``````>>> timesync_addr = IPv4Address("169.254.169.123")
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
``````>>> IPv4Address("10.243.156.214").is_private
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:

• `0.0.0.0/8` is used for “this host on this network.”
• `127.0.0.0/8` is used for loopback addresses.
• `169.254.0.0/16` is used for link-local addresses as discussed above.
• `198.18.0.0/15` is used for benchmarking the performance of 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
``````>>> addr = IPv4Address("220.14.9.37")
3691907365
``````
Copied!

The `._ip` attribute is actually what’s responsible for producing `int(addr)`. The chain of calls is that `int(my_addr)` calls `my_addr.__int__()`, which `IPv4Address` implements as just `my_addr._ip`:

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.

### Extending `IPv4Address`

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

Python
``````from ipaddress import IPv4Address

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
``````>>> addr = MyIPv4("100.127.40.32")
>>> 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 takes advantage of operator overloading to allow you to infer relations between addresses and networks.
• 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.

Here are some in-depth resources that you can check out to learn more about the `ipaddress` module:

Copied!
Happy Pythoning!

🐍 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?