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.
Free Download: Get a sample chapter from CPython Internals: Your Guide to the Python 3 Interpreter showing you how to unlock the inner workings of the Python language, compile the Python interpreter from source code, and participate in the development of CPython.
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:
>>> (
... 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:
$ 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.
Note: In the interest of technical correctness, this is quite possibly not your computer’s very own public IP address. If your connection sits behind a NATed router, then it’s better thought of as an “agent” IP through which you reach the Internet.
Now open up a Python REPL. You can use the IPv4Address
class to build a Python object that encapsulates an address:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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.
Note: This tutorial focuses on Internet Protocol version 4 (IPv4) addresses. There are also IPv6 addresses, which are 128-bit rather than 32-bit and are expressed in a headier form such as 2001:0:3238:dfe1:63::fefb
. Since the arithmetic of addresses is largely the same, this tutorial cuts one variable out of the equation and focuses on IPv4 addresses.
The ipaddress
module features a more flexible factory function, ip_address()
, which accepts an argument that represents either an IPv4 or an IPv6 address and does its best to return either an IPv4Address
or an IPv6Address
instance, respectively.
In this tutorial, you’ll cut to the chase and build address objects with IPv4Address
directly.
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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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
:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> net = IPv4Network("192.4.2.0/28")
>>> for addr in net:
... print(addr)
...
192.4.2.0
192.4.2.1
192.4.2.2
...
192.4.2.13
192.4.2.14
192.4.2.15
Similarly, net.hosts()
returns a generator that will yield the addresses shown above, excluding the network and broadcast addresses:
>>> h = net.hosts()
>>> type(h)
<class 'generator'>
>>> next(h)
IPv4Address('192.4.2.1')
>>> next(h)
IPv4Address('192.4.2.2')
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:
>>> 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
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:
net = IPv4Network("200.100.10.0/24")
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:
>>> for sn in net.subnets():
... print(sn)
...
200.100.10.0/25
200.100.10.128/25
You can also tell .subnets()
what the new prefix should be. A higher prefix means more and smaller subnets:
>>> 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
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:
>>> from ipaddress import IPv4Interface
>>> ifc = IPv4Interface("192.168.1.6/24")
>>> ifc.ip # The host IP address
IPv4Address('192.168.1.6')
>>> ifc.network # Network in which the host IP resides
IPv4Network('192.168.1.0/24')
Above, 192.168.1.6/24
means “the IP address 192.168.1.6
in the network 192.168.1.0/24
.”
Note: In the context of computer networking, interface also may refer to a network interface, most commonly a network interface card (NIC). If you’ve ever used the ifconfig
tool (*nix) or ipconfig
(Windows), then you may know yours by a name such as eth0
, en0
, or ens3
. These two types of interfaces are unrelated.
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.
Special Address Ranges
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:
Range | Number of Addresses | Network Address | Broadcast Address |
---|---|---|---|
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:
>>> IPv4Address("10.243.156.214") in IPv4Network("10.0.0.0/8")
True
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:
>>> timesync_addr = IPv4Address("169.254.169.123")
>>> timesync_addr.is_link_local
True
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:
>>> IPv4Address("10.243.156.214").is_private
True
>>> IPv4Address("127.0.0.1").is_loopback
True
>>> [i for i in dir(IPv4Address) if i.startswith("is_")] # "is_X" properties
['is_global',
'is_link_local',
'is_loopback',
'is_multicast',
'is_private',
'is_reserved',
'is_unspecified']
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.
Note: To be fair, the ipaddress
module also uses a healthy dose of inheritance, mainly to reduce code duplication.
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:
>>> addr = IPv4Address("220.14.9.37")
>>> addr._ip
3691907365
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:
from ipaddress import IPv4Address
class MyIPv4(IPv4Address):
def __and__(self, other: IPv4Address):
if not isinstance(other, (int, IPv4Address)):
raise NotImplementedError
return self.__class__(int(self) & int(other))
Adding .__and__()
lets you use the binary AND (&
) operator. Now you can directly apply a netmask to a host IP:
>>> addr = MyIPv4("100.127.40.32")
>>> mask = MyIPv4("255.192.0.0") # A /10 prefix
>>> addr & mask
MyIPv4('100.64.0.0')
>>> addr & 0xffc00000 # Hex literal for 255.192.0.0
MyIPv4('100.64.0.0')
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.
Besides operator overloading, you could add brand new properties as well:
1import re
2from ipaddress import IPv4Address
3
4class MyIPv4(IPv4Address):
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)
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:
- It removes anything besides 0s and 1s from the input string.
- 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:
>>> 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')
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.
Further Reading
Here are some in-depth resources that you can check out to learn more about the ipaddress
module: