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
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- 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
- 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
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'))True
FalseThe text prefix invents membership; the parsed network checks the actual bits.
02. Parse both IP families
import ipaddress
for text in ('192.168.1.42', '2001:db8::7'):
address = ipaddress.ip_address(text)
print(address.version, address.compressed)4 192.168.1.42
6 2001:db8::7Parsed objects expose the address family and a normalized display form.
03. Reject host bits in a network declaration
import ipaddress
try:
ipaddress.ip_network('192.168.1.7/24')
except ValueError:
print('host bits rejected')host bits rejectedStrict parsing catches a common configuration mistake.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
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.
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'))True
False- Compare the misleading string-prefix result with CIDR membership.
- Treat addresses and network ranges as parsed values.
- Do not infer trust from an address supplied by an untrusted caller.
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.
import ipaddress
for text in ('192.168.1.42', '2001:db8::7'):
address = ipaddress.ip_address(text)
print(address.version, address.compressed)4 192.168.1.42
6 2001:db8::7- Parse each address before extracting its family.
- Inspect the normalized output rather than splitting on punctuation.
- Distinguish a valid nonmember from invalid syntax.
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.
import ipaddress
try:
ipaddress.ip_network('192.168.1.7/24')
except ValueError:
print('host bits rejected')host bits rejected- Pass a noncanonical address with a /24 prefix.
- Observe the strict-parsing error instead of masking it.
- 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 runNeed input()? Add one value per line
Your output appears here.
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.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Matching text prefixes
address.startswith('192.168.1')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
ipaddress.ip_network(text, strict=False)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.