LESSON 94 · PRACTICAL PYTHON28 min

Validate IP Addresses and Networks with ipaddress

Parse addresses and CIDR networks, then decide membership with ipaddress rather than text matching.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Validate IP Addresses and Networks with ipaddress

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    IP addresses look like strings, so it is tempting to validate them or check membership with string methods. That leads to mistakes. The text `192.0.2.10` begins with `192.0.2.1`, but it is a different address. Network ranges also have bit boundaries that are not obvious from decimal prefixes. Python's `ipaddress` module parses addresses and networks into objects that understand those boundaries. We will first reproduce a false match, then use `ip_address` and `ip_network` to check actual membership. After that, we will make a small function that rejects invalid input rather than silently guessing.

  2. 00:42

    Our examples use documentation address ranges, not an actual network service. We do not connect to any host. One more caution: deciding whether an address belongs to a network is only one piece of access control. A production system must also decide which peer address is trustworthy, especially behind proxies. For this lesson we focus on representing and validating addresses correctly before any policy decision is made. The first candidate is exactly the allowed host. The second has one extra zero at the end, but `startswith` only compares text from the beginning. It does not know where one address component ends or what a network mask

  3. 01:24

    means. If this check controlled a sensitive action, the false match would be a real defect. Switching to `split('.')` and comparing pieces by hand may fix this single example, but it still leaves address validation, IPv6, and network masks for us to reimplement. A textual prefix like `192.0.2.` happens to describe a familiar group of IPv4 addresses, but it is not a general implementation of CIDR network membership. It also does not reject invalid forms such as a component outside the allowed range. Instead, parse the input. If parsing fails, treat it as invalid.

  4. 02:06

    If parsing succeeds, compare address objects or ask whether an address is contained in a network object. The standard library already knows the rules for both IPv4 and IPv6. The two parsed addresses are different objects representing different numeric addresses. Both belong to the same /24 network, which has 256 possible IPv4 addresses. That makes the distinction explicit: exact host equality asks a narrower question than network membership. A network with a /32 prefix would contain only one IPv4 address. The mask size is part of the policy, not a textual convenience.

  5. 02:47

    The `ipaddress` module also supports IPv6 objects, so the same basic pattern applies without splitting on colons yourself. The call `ip_network` is strict by default about host bits in a network address. For example, `192.0.2.1/24` is a host with a mask, not the canonical network base, and strict parsing will reject it. Use `strict=False` only when you deliberately want the module to normalize that input to its containing network. In a validation path, silently normalizing a surprising value can hide a configuration error, so decide what the caller is allowed to supply.

  6. 03:28

    The function converts both inputs before making a decision. If either value is malformed, it raises a clear validation error and preserves the original cause. If the address and network have different versions, such as IPv4 versus IPv6, membership is False. Otherwise it delegates containment to the parsed network. Our first example belongs to the documentation /24 range; the second is outside it. The result is a Boolean based on actual address bits, not a decimal string prefix. In a real service, you would build the allowed network objects once from trusted configuration rather than reparsing them for every request.

  7. 04:10

    You would also carefully determine the true remote peer address. Headers supplied by clients can be spoofed unless a trusted proxy has established them. This helper does not claim to solve those deployment questions; it solves the local parsing and membership task. Keeping that boundary narrow makes it easier to test and reuse. The /32 test catches the false match from the beginning: `192.0.2.10` is not the host `192.0.2.1`. We also test an IPv6 documentation address inside an IPv6 range and a mixed-version check that must return False. Finally, the address with 999 in one component is invalid and raises

  8. 04:52

    `ValueError`. Catching it in the test confirms that bad data is rejected, not accepted after a string comparison. On the site, change the prefix from /32 to /24 and predict which assertion changes. Then try a different IPv6 prefix and observe the containment rule. Be precise about the question you ask: exact host identity, membership in a network, and validity of the input are three separate checks. Parsed objects make those differences visible in code. Complete the practice task by replacing a misleading textual test with an explicit network rule and at least one negative case.

Understand the concept

An IP address is structured data, not an arbitrary string. A text prefix such as 192.168.1 also matches 192.168.10, and a suffix comparison tells us nothing about IPv6. The standard-library ipaddress module parses addresses into objects with numerical meaning. ip_address accepts IPv4 or IPv6 and rejects malformed text with ValueError. Display the parsed version before applying a network policy.

A CIDR network combines an address with a prefix length. ip_network('192.168.1.0/24') represents 256 IPv4 addresses; membership uses the network bits, not the first characters of a string. The strict=True default rejects a host address such as 192.168.1.7/24 as a network identifier. Keeping that default prevents a mistyped network from silently becoming a broader rule.

The checked function address_in_network receives two strings. It parses each, rejects invalid syntax, and returns False when the address and network use different IP versions. Malformed input is an error, whereas a valid IPv6 address simply is not inside an IPv4 network. The caller can choose whether to surface an error as a form message or a rejected configuration.

Test the lower and upper ends of a network, a neighboring network with a similar text prefix, IPv6 membership, mixed versions, and a noncanonical network. This is a membership lesson, not a firewall implementation. Real access control also needs trusted source addresses, proxy handling, and a clear policy for special address ranges.

  • IP addresses are parsed values
  • CIDR membership uses network bits
  • Strict networks reject host bits
  • IPv4 and IPv6 versions must agree

See it step by step

Read the code, predict the output, then compare it with the result.

01. Expose a false string-prefix match

PYTHON
print('192.168.10.5'.startswith('192.168.1'))
import ipaddress
print(ipaddress.ip_address('192.168.10.5') in ipaddress.ip_network('192.168.1.0/24'))
EXPECTED OUTPUT
True
False

The text prefix invents membership; the parsed network checks the actual bits.

02. Parse both IP families

PYTHON
import ipaddress
for text in ('192.168.1.42', '2001:db8::7'):
    address = ipaddress.ip_address(text)
    print(address.version, address.compressed)
EXPECTED OUTPUT
4 192.168.1.42
6 2001:db8::7

Parsed objects expose the address family and a normalized display form.

03. Reject host bits in a network declaration

PYTHON
import ipaddress
try:
    ipaddress.ip_network('192.168.1.7/24')
except ValueError:
    print('host bits rejected')
EXPECTED OUTPUT
host bits rejected

Strict parsing catches a common configuration mistake.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Prove that a text prefix is not a network

Start with an address that looks nearly identical to the permitted prefix: 192.168.10.5. A plain startswith call sees the characters 192.168.1 and returns True. That result is a false match because the third octet is ten, not one. Text formatting does not encode the boundary between network bits and host bits.

Parse the same address and a /24 network with ipaddress, then ask whether the address object is a member. The False result is the meaningful policy decision. Before using this in access control, remember that a request's claimed source address can be untrustworthy; this exercise teaches address arithmetic, not network identity verification.

PYTHON
print('192.168.10.5'.startswith('192.168.1'))
import ipaddress
print(ipaddress.ip_address('192.168.10.5') in ipaddress.ip_network('192.168.1.0/24'))
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. Compare the misleading string-prefix result with CIDR membership.
  2. Treat addresses and network ranges as parsed values.
  3. Do not infer trust from an address supplied by an untrusted caller.
02 / 03

Normalize IPv4 and IPv6 without hand parsing

The second run feeds one IPv4 and one IPv6 address to the same ip_address function. It prints each object's version and compressed form. This makes family differences explicit, while the public API handles details such as IPv6 zero compression that are easy to mishandle with custom string operations.

A valid address outside a network is a normal False result; malformed text is an error. Keep those outcomes separate in the checked function. If an IPv6 address is compared with an IPv4 network, return False instead of coercing one representation into the other. A caller can then display a precise validation message for truly invalid text.

PYTHON
import ipaddress
for text in ('192.168.1.42', '2001:db8::7'):
    address = ipaddress.ip_address(text)
    print(address.version, address.compressed)
EXPECTED OUTPUT
4 192.168.1.42
6 2001:db8::7
Follow the reasoning
  1. Parse each address before extracting its family.
  2. Inspect the normalized output rather than splitting on punctuation.
  3. Distinguish a valid nonmember from invalid syntax.
03 / 03

Reject a mistyped network declaration

The final example passes 192.168.1.7/24 as if it were the network identifier. With strict parsing, ip_network raises ValueError because host bits are set. Catching and printing a short diagnostic makes that boundary visible. Silently normalizing a configuration typo into a broader network would hide an important mistake.

For a completed membership function, test the first and last addresses in a declared range, then test the similar-looking neighbor. Add one IPv6 case and a mixed-family case. Keep strict behavior unless the application explicitly intends to normalize a host-with-prefix input; document that choice at the call site.

PYTHON
import ipaddress
try:
    ipaddress.ip_network('192.168.1.7/24')
except ValueError:
    print('host bits rejected')
EXPECTED OUTPUT
host bits rejected
Follow the reasoning
  1. Pass a noncanonical address with a /24 prefix.
  2. Observe the strict-parsing error instead of masking it.
  3. Verify boundaries and different address families.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete address_in_network using ipaddress.ip_address and ipaddress.ip_network with strict network parsing. Return False for a valid address and network of different IP versions. Let invalid address or noncanonical network text raise ValueError. Prepared output must be False then True.
Need a hint?

Parse both inputs first. Compare their .version values; when equal, return address in network. Do not compare textual prefixes.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why is startswith unsafe for network membership?

    02.What does ip_network('192.168.1.7/24') do by default?

    03.What should valid IPv6 versus a valid IPv4 network return here?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Matching text prefixes

    COMMON MISTAKE
    address.startswith('192.168.1')
    FIX
    ipaddress.ip_address(address) in ipaddress.ip_network('192.168.1.0/24')

    What happens: 192.168.10.5 is incorrectly accepted.
    Use CIDR arithmetic for membership.

    Silently normalizing a mistyped network

    COMMON MISTAKE
    ipaddress.ip_network(text, strict=False)
    FIX
    ipaddress.ip_network(text)

    What happens: A host address can turn into a larger access range without notice.
    Strict parsing makes the configuration error visible.

    Browse this lesson’s problem fixes

    Sources and further reading