Python Training by Dan Bader

An Overview of Python’s “ipaddress” Module

An introduction to the ipaddress module available on Python 3.3+ for manipulation of IPv4 and IPv6 addresses.

Python 3 ipaddress Module Overview

In this article we’ll take a look at the ipaddress module that is available on Python 3.3 and above. This tutorial is intended to serve as a handy reference for any network engineer wondering how to parse and work with IP addresses in Python.

In this overview article you’ll learn:

  • What the differnce between IPv4 and IPv6 addresses is.
  • How to work with IPv4 addresses using Python’s ipaddress module.
  • How to work with IPv6 addresses using Python’s ipaddress module.

IPv4 vs IPv6 Addresses – A Primer

At a high level, IPv4 and IPv6 addresses are used for like purposes and functions. However, since there are major differences in the address structure for each protocol, this tutorial has separated into separate sections, one each for IPv4 and IPv6.

In today’s Internet, the IPv4 protocol controls the majority of IP processing and will remain so for the near future. The enhancements in scale and functionality that come with IPv6 are necessary for the future of the Internet and adoption is progressing. The adoption rate, however, remains slow to this date.

An IPv4 address is composed of 32 bits, organized into four eight bit groupings referred to as “octets”. The word “octet” is used to identify an eight-bit structure in place of the more common term “byte”, but they carry the same definition. The four octets are referred to as octet1, octet2, octet3, and octet4. This is a “dotted decimal” format where each eight-bit octet can have a decimal value based on eight bits from zero to 255.

IPv4 address example: 192.168.100.10

IPv4 address example (CIDR notation): 192.168.100.10/24

The /24 is CIDR notation to indicate that leading 24 of the 32 bits are used to identify the network portion of the address. Remembering that each octet is 8 bits long, this means that the first three octets (3 × 8 = 24) identify the network (192.168.100.x) and the remaining eight bits of the address identify the node (x.x.x.10).

CIDR notation can be anything from /8 bits through to /30 bits, with an occasional /32 bits (/31 is invalid), but /24 is often used. For example, your home network, or your school or company network is most likely identified with a /24 CIDR.

An older format for expressing the network identification is a network mask where the CIDR is expressed as a separate dotted decimal number. For example, a /24 CIDR equates to a network mask of 255.255.255.0.

An IPv6 address is 128 bits long, which is a significant increase over the 32 bits in an IPv4 address. There are many differences between IPv4 and IPv6, but the notable difference is in the addressing structure. The additional length provides an exponential increase in the number of networks and host that can be supported.

IPv6 address example: 2001:db8:abcd:100::1/64

Where the IPv4 address uses a dotted decimal format, the IPv6 protocol uses hexadecimal notation. Each position in an IPv6 address represents four bits with a value from 0 to f, organized as follows:

  • The 128 bits are divided into 8 groupings of 16 bits each separated by colons. A group is referred to as a “quartet” or “hextet” each with four hexadecimal characters (4 hex characters times 4 bits = 16 bits). In the above example, the first quartet is “2001”.
  • Leading zeros in any quartet are suppressed/condensed. In the above example, the second quartet is “db8”, which is actually “0db8”” with the leading zero suppressed. The last quartet is “1”, which is actually “0001”” with three leading zeros suppressed.
  • If a quartet contains all zeros, it is suppressed to a single zero. For example: a quartet with “:0000:” would be compressed to “:0:”.
  • If an address contains a contiguous string of quartets that are all zeros, the contiguous string of zeros is condensed and represented with double colons. In the above example, the double colon represents three all zero quartets, or “:0000:0000:0000:” condensed to “::”. Since the example address has five quartets with values, the number of condensed quartets must be three (eight total minus five populated).

All IPv6 address structures used CIDR notation to determine how many of the leading bits are used for network identification with the balance used for host/interface identification. Given 128 bits, many options are available.

Python’s ipaddress Module and IPv4 Addresses

The ipaddress module is designed around CIDR notation, which is recommended because of its brevity and ease of use. The ipaddress module also includes methods to revert to a network mask if required.

The original definition of IPv4 addresses includes a “class” that is defined by address ranges in the first octet. The ipaddress module does not recognize IPv4 classes and is therefore not included in this tutorial.

The ipaddress module includes three specific IPv4 address object types:

  1. a “host” or an individual address object that does not include CIDR notation,
  2. an individual interface address object that includes CIDR notation, and
  3. and a network address object that refers to the range of IP addresses for the entire network.

The major difference between a “host” and an “interface” is that a host or ip_address object does not include CIDR notation, whereas an ip_interface object includes the CIDR notation:

  • The ip_address object is most useful when working with IP packets that do not need nor use CIDR notation.
  • The ip_interface object is most useful when working with node and interface identification for connection to an IP network which must include network/subnet identification.
  • The ip_network object includes all addresses within a network and is most useful for network identification.

Creating IPv4 Host Address Objects with ipaddress:

The ipaddress.ip_address() factory function is used to create an ip_address object. This automatically determines whether to create an IPv4 or IPv6 address based on the passed-in value (IPv6 addressing will be discussed at a latter point in this tutorial). As noted above, this object represents an IP Address as found in a packet traversing a network where CIDR is not required.

In many cases, the value used to create an ip_address object will be a string in the IPv4 dotted decimal format as per this example:

>>> import ipaddress
>>> my_ip = ipaddress.ip_address('192.168.100.10')
>>> my_ip
IPv4Address('192.168.100.10')

Alternatively, the IPv4 address may be entered in binary, as a decimal value of the full 32 bit binary value, or in hexadecimal format as per this example:

# All 32 binary bits can be used to create an IPv4 address:
>>> ipaddress.ip_address(0b11000000101010000110010000001010)
IPv4Address('192.168.100.10')

# The decimal value of the 32 bit binary number can also be used:
>>> ipaddress.ip_address(3232261130)
IPv4Address('192.168.100.10')

# As can the hexadecimal value of the 32 bits:
>>> ipaddress.ip_address(0xC0A8640A)
IPv4Address('192.168.100.10')

The first example uses the full 32 bit address, and the second example is the decimal value of the 32 bit address. Both are unwieldy, error-prone and of limited value. The third example uses a hexadecimal value which can be useful as most packet formats from parsing or sniffing are represented in hexadecimal format.

Creating IPv4 Interface Address Objects with ipaddress:

The ipaddress.ip_interface() factory function is used to create an ip_interface object, which automatically determines whether to create an IPv4 or IPv6 address based on the passed-in value (IPv6 addressing will be discussed at a latter point in this tutorial).

As previously discussed, the ip_interface object represents the ip address found on a host or network interface where the CIDR (or mask) is required for proper handling of the packet.

# An ip_interface object is used to represent IP addressing
# for a host or router interface, including the CIDR:
>>> my_ip = ipaddress.ip_interface('192.168.100.10/24')
>>> my_ip
IPv4Interface('192.168.100.10/24')

# This method translates the CIDR into a mask as would normally
# be used on a host or router interface
>>> my_ip.netmask
IPv4Address('255.255.255.0')

One can use the same options in the creation of an ip_interface option as with an ip_address option (binary, decimal value, hexadecimal). However, the only way to effectively create an ip_interface with the proper CIDR notation or mask is with a dotted decimal IPv4 address string.

Creating IPv4 Network Address Objects with ipadress:

The ipaddress.ip_network() factory function is used to create an ip_network object, which automatically determines whether to create an IPv4 or IPv6 address based on the passed-in value (IPv6 addressing will be discussed at a latter point in this tutorial).

An IP network is defined as a range of consecutive IP address that define a network or subnet. Example:

  • 192.168.100.0/24 is the 192.168.100.0 network where the /24 specifies that the first three octets comprise the network identification.
  • The 4th octet is used for assignment to individual hosts and router interfaces.
  • The address range is 192.168.100.1 through to .254.
  • 192.168.100.0 is used to define the network/subnet and 192.168.100.255 is the broadcast address for this network. Neither can be used for assignment to a host or router interface.

The creation of an ip_network object follows the same syntax as the creation of an ip_interface object:

# Creates an ip_network object. The IPv4 address and CIDR must be
# a valid network address, the first address in an address range:
>>> ipaddress.ip_network('192.168.100.0/24')
IPv4Network('192.168.100.0/24')

In the above example, the network address used must be a valid network address, which is the first address in the range of IPv4 addresses that constitute the network. If this is not the case, Python will throw an exception:

# Python will throw an exception if the address used is not
# a valid network address. In the following, ".10" is a host address
# not a valid network address ident cation, which is ".0":
>>> ipaddress.ip_network('192.168.100.10/24')
ValueError: "192.168.100.10/24 has host bits set"

When working with host or router interfaces, it is often necessary to determine the network address. This can be calculated, but takes several steps which can be accomplished in a single step using the strict=False option (strict=True is default).

# If the network address needs to be calculated,
# use the strict=False option. This will calculate and populate
# the ip_network object with the network rather than the
# interface address:
>>> my_ip = ipaddress.ip_interface('192.168.100.10/24')
>>> my_ip
IPv4Interface('192.168.100.10/24')

>>> my_ip_net = ipaddress.ip_network(my_ip, strict=False)
>>> my_ip_net
IPv4Network('192.168.100.0/24')

In the above example, the ip_interface address is known (192.168.100.10) but not the ip_network the interface belongs to. Using the strict=False option, the ip_network address (192.168.100.0/24) is calculated and populated in the ip_network object.

Python’s ipaddress Module and IPv6 Addresses

As with IPv4, the ipaddress module uses the same three basic factory functions already described for IPv4. includes include:

  1. a “host” or an individual address object that does not include CIDR notation,
  2. an interface address object that includes CIDR notation, and
  3. and a network address object that refers to the range of IP addresses for the entire network.

Since the detail is covered in the section on IPv4, a brief overview is only necessary.

Creating IPv6 Host Address Objects with ipaddress:

The ipaddress.ip_address() factory function is used to create an ip_address object. This automatically knows to use the IPv6 address format based on the passed-in value. Note that the CIDR notation is not used with the ip_address function.

In the majority of cases, the value used to create an ip_address object for IPv6 will be a string in the IPv6 quartet/hextet format as per this example:

# Create an IPv6 Address Object for a Global Address:
>>> ipaddress.ip_address('2001:db8:abcd:100::1')
IPv6Address('2001:db8:abcd:100::1')

# Create an IPv6 Address Object for a link-local address:
>>> ipaddress.ip_address('fe80::1')
IPv6Address('fe80::1')

As with IPv4, it is possible to create an IPv6 address object using the full binary, decimal, or hexadecimal value. This is unwieldy with 32 bits for an IPv4 address and is even more awkward for a 128 bit IPv6 address. As a practical matter, it is anticipated that the string representation of the eight quartets will be the norm.

Creating IPv6 Interface Address Objects with ipaddress:

The ipaddress.ip_interface() factory function is used to create an ip_interface object, which automatically create an IPv6 address based on the passed-in value. Note that the CIDR notation must be included in the function.

# Creates an IP Interface Object for a Global Address:
>>> ipaddress.ip_interface('2001:db8:abcd:100::1/64')
IPv6Interface('2001:db8:abcd:100::1/64')

# Creates an IP Interface Object for a Link-local Address:
ipaddress.ip_interface('fe80::1/64')
IPv6Interface('fe80::1/64')

Creating IPv6 Network Address Objects with ipaddress:

The ipaddress.ip_network() factory function is used to create an ip_network object for IPv6 based on the passed-in value.

As with IPv4, an IPv6 network is defined as a range of consecutive IP address that can be assigned to specific host or router interfaces.

Using our previous example 2001:db8:abcd:100::/64, the /64 CIDR specifies that the four quartets make up the full network identification. Remember that the first three quartets are global ID assigned by the IPS and the fourth quartet identifies the internal subnet number. The balance of the 64 bits are used for host identification with a range from “0000:0000:0000:0001” though to “ffff:ffff:ffff:fffe”.

As with IPv4 addressing, the first and last address in an IPv6 subnet cannot be used for host addressing. Given a /64 CIDR, this means that there are 2 to the 64th power (minus 2) possible host addresses, which is means there are 18,446,744,073,709,551,614 mathematically possible host addresses per network/subnet.

# Creates an IP Network Object for a Global Address:
>>> myIPv6net = ipaddress.ip_network('2001:db8:abcd:100::/64')
>>> myIPv6net
IPv6Network('2001:db8:abcd:100::/64')

# Creates an IP Network Object for a Link-local Address:
>>> myIPv6 = ipaddress.ip_network('fe80::/64')
>>> myIPv6
IPv6Network('fe80::/64')

The above global address is broken down as follows:

  • Global Identifier assigned by ISP: 2001:db8:abcd::/48
  • Subnet identification: 2001:db8:abcd:100::/64
  • First usable address in the subnet: 2001:db8:abcd:100::1/64
  • Last usable address in the subnet: 2001:db8:abcd:100:ffff:ffff:ffff:fffeffff/64

Additional Resources

These are some additional resources where you can learn about the ipaddress module in Python:

<strong><em>Improve Your Python</em></strong> with a fresh 🐍 <strong>Python Trick</strong> 💌 every couple of days

Improve Your Python with a fresh 🐍 Python Trick 💌 every couple of days

🔒 No spam ever. Unsubscribe any time.

This article was filed under: modules, and python.

Related Articles:

About the Author

Jon Crawfurd

Jon Crawfurd is a network and security instructor at the college level. He is retired from a long career at Cisco Systems, and holds several Cisco and Security Certifications. He's taken up Python programming for automated network management applications, and because he likes it!

Latest Articles:
← Browse All Articles