Build a Subnet Calculator CLI Tool

Build a zero-dependency Python CLI that computes full subnet details from CIDR notation.

Introduction

30 Second Summary

Every device on a network needs an address, and every network needs boundaries. Understanding how IP addresses get divided into subnets is one of those foundational skills that separates someone who uses networks from someone who builds them.

In this project, you will build a command-line subnet calculator that takes any IP address in CIDR notation and outputs a complete network breakdown, including a color-coded binary visualization that shows exactly where network bits end and host bits begin.

What You'll Build

A zero-dependency Python CLI tool that computes subnet details, visualizes binary bit boundaries with ANSI color codes, generates VLSM subdivision tables, and exports results to JSON.

By the end of this project, you'll have:

  • A working CLI tool that computes and displays full subnet details from any IP/CIDR notation input.
  • A color-coded binary visualization that separates network bits from host bits, reinforcing how subnet masks actually work.
  • A VLSM subdivision table and JSON export capability with proper error handling and clean degradation when piped.
  • Secret Mission: Add a --plan flag that accepts comma-separated host requirements and outputs an optimal VLSM allocation plan.

Are there any prerequisites?

You should be comfortable with Python basics like functions, string formatting, and conditionals. Familiarity with IP addresses is helpful but not required. The project explains networking concepts as you encounter them.

Set Up the Project Structure

Before building any networking logic, you need a working project folder and a confirmed Python environment.

In this step, you will verify that Python is ready, create the project directory and main file, and wire up a minimal CLI skeleton using argparse.

In this step, get ready to:
  • Confirm Python 3.9+ is installed and working.
  • Create the project directory and main file.
  • Set up a minimal argparse CLI skeleton that accepts a network argument.
Verify Python and create the project directory

This project uses Python's built-in ipaddress and argparse modules, so you need Python 3.9 or higher.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.9 or higher

You are good to go. Your output should look something like Python 3.9.6 or any version above 3.9.

ⓧ I see an older version

You need Python 3.9 or higher for this project. Visit the official downloads page to get the correct version:

https://www.python.org/downloads/

  • Download the Python 3.9+ installer for your operating system.
  • Run the installer and follow the prompts. On Windows, make sure to check Add Python to PATH.
  • After installing, close and reopen your terminal, then run python3 --version again to confirm.

ⓧ Command not found

Python is not installed on your system. Visit the official downloads page:

https://www.python.org/downloads/

  • Download the Python 3.9+ installer for your operating system (macOS or Windows).
  • Run the installer and follow the prompts. On Windows, make sure to check Add Python to PATH.
  • After installing, close and reopen your terminal, then run python3 --version again to confirm you see a version number.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir subnet-calculator
cd subnet-calculator
  • Create the main Python file by running:
touch subnetcalc.py
  • Confirm the files exist by running:
ls

You should see subnetcalc.py listed in the output.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the subnet-calculator folder, then click Open.

Why open the folder in VS Code?

Opening the project folder (not just the file) gives VS Code full context. The Explorer sidebar will show your project structure, and the integrated terminal will default to your project directory.

Don't see subnetcalc.py in the Explorer sidebar?

Make sure you opened the subnet-calculator folder itself, not a parent folder. The file should appear in the left sidebar once the correct folder is open.

help me open my project folder in VS Code

Confirm the ipaddress module works

The ipaddress module ships with Python's standard library. A quick test confirms it loads and processes a network correctly.

  • Open subnetcalc.py from the Explorer sidebar by clicking on it.
  • Add this test code to confirm the module works:
import ipaddress

# Quick test to confirm the ipaddress module works
network = ipaddress.ip_network('192.168.1.0/24', strict=False)
print(f"Network: {network.network_address}")
print(f"Broadcast: {network.broadcast_address}")
print(f"Netmask: {network.netmask}")
print(f"Hostmask: {network.hostmask}")

What does this code do?

This creates an IPv4Network object from a CIDR string and prints four key properties. The strict=False parameter tells Python to accept host addresses like 192.168.1.50/24 and auto-mask them to the network address.

  • Save subnetcalc.py.
  • Run the test by entering this command in the terminal:
python3 subnetcalc.py

You should see this output:

Network: 192.168.1.0
Broadcast: 192.168.1.255
Netmask: 255.255.255.0
Hostmask: 0.0.0.255

Seeing a ModuleNotFoundError?

The ipaddress module is part of Python's standard library and should always be available. If you see an import error, your Python installation may be incomplete. Reinstall Python from python.org/downloads.

help me fix a ModuleNotFoundError for ipaddress

Set up the argparse CLI skeleton

Now that you know the ipaddress module works, replace the test code with the real CLI structure. The argparse module lets you define command-line arguments and auto-generates help text.

  • Select all the code in subnetcalc.py and delete it.
  • Replace it with this argparse skeleton:
#!/usr/bin/env python3
"""Subnet Calculator CLI Tool."""

import argparse

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    args = parser.parse_args()
    print(args.network)

if __name__ == "__main__":
    main()

What does this code do?

This sets up a proper CLI entry point. argparse.ArgumentParser creates a parser with a description that appears in --help output.

The add_argument("network") call defines a required positional argument. The if __name__ == "__main__" guard ensures main() only runs when you execute the file directly.

  • Save subnetcalc.py.
  • Test the CLI by running this command:
python3 subnetcalc.py 192.168.1.0/24

You should see 192.168.1.0/24 printed back to the terminal.

Seeing "error: the following arguments are required: network"?

This means you ran the script without passing a network argument. Make sure to include a CIDR string after the filename, like python3 subnetcalc.py 192.168.1.0/24.

help me fix my argparse argument error

✔️ Awesome, I've got everything!

Great. Double check you have saved subnetcalc.py and your terminal echoed the network string back to you.

ⓧ I'd like to double check the full code

Here is the complete subnetcalc.py at this point:

#!/usr/bin/env python3
"""Subnet Calculator CLI Tool."""

import argparse

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    args = parser.parse_args()
    print(args.network)

if __name__ == "__main__":
    main()

Your project structure is ready and the CLI accepts input. Next up, you will build the core calculation engine that turns that input into a full subnet breakdown.

Build the Core Calculation Engine

Your CLI skeleton can accept network input, but it does nothing with it yet. In this step, you will build the calculation engine that turns a CIDR string into a full subnet breakdown.

You will write three core functions: one to compute all subnet details, one to generate a binary representation of the address, and one to display everything with color-coded output in the terminal.

In this step, get ready to:
  • Build the calculate_subnet function that computes network details from CIDR input.
  • Build the get_binary_breakdown function that visualizes network vs host bits.
  • Build the color-coded display system with ANSI escape codes and a formatted output function.
Build the calculate_subnet function

Python's built-in ipaddress module handles all the RFC-compliant networking math for you. Your function will use it to extract every piece of subnet information from a single CIDR notation string.

  • In subnetcalc.py, find the line that says import argparse, then add these three new imports directly below it:
import ipaddress
import os
import sys

Why three new imports?

The ipaddress module handles network address parsing and math. The os and sys modules help detect whether the terminal supports color output.

  • Now add the calculate_subnet function between the imports and the existing def main(): function. Leave a blank line after the imports, then paste this code:
# --- Subnet Calculation ---

def calculate_subnet(cidr_input):
    """Calculate all subnet details from a CIDR string."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    hosts_list = list(network.hosts())
    num_hosts = len(hosts_list)

    if num_hosts > 0:
        first_host = str(hosts_list[0])
        last_host = str(hosts_list[-1])
    else:
        first_host = "N/A"
        last_host = "N/A"

    return {
        "network_address": str(network.network_address),
        "broadcast_address": str(network.broadcast_address),
        "subnet_mask": str(network.netmask),
        "wildcard_mask": str(network.hostmask),
        "prefix_length": network.prefixlen,
        "first_host": first_host,
        "last_host": last_host,
        "usable_hosts": num_hosts,
        "total_addresses": network.num_addresses,
    }

What does this code do?

  • ipaddress.ip_network(cidr_input, strict=False) creates a network object from the CIDR string. The strict=False flag lets users type host addresses like 192.168.1.50/24 without getting an error. It auto-masks to the correct network address.
  • network.hosts() returns all usable host addresses (excluding the network and broadcast addresses). You grab the first and last to show the usable range.
  • The function returns a dictionary with every piece of subnet information you need for the display later.
  • Save subnetcalc.py.

You won't see a change yet in the terminal output. The next chunk uses this function.

Build the get_binary_breakdown function

Subnet masks are really about binary bits. A /24 prefix means the first 24 bits identify the network, and the remaining 8 bits identify hosts. This function converts the network address to binary so you can visualize that split.

  • In subnetcalc.py, add the get_binary_breakdown function directly below calculate_subnet by pasting this code:
def get_binary_breakdown(cidr_input):
    """Generate binary representation showing network vs host bits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    addr_int = int(network.network_address)
    binary_str = format(addr_int, "032b")

    octets = [binary_str[i:i + 8] for i in range(0, 32, 8)]

    return {
        "binary_octets": octets,
        "prefix_length": prefix_len,
        "network_bits": binary_str[:prefix_len],
        "host_bits": binary_str[prefix_len:],
    }

What does this code do?

  • format(addr_int, "032b") converts the IP address integer into a 32-character binary string (padded with leading zeros).
  • The list comprehension slices that 32-bit string into four 8-bit octets for display.
  • binary_str[:prefix_len] and binary_str[prefix_len:] split the bits into network portion and host portion based on the prefix length.
  • Save subnetcalc.py.

You still won't see a change in terminal output. The display function in the next chunk brings both of these functions to life.

Build the display with ANSI color support

Terminal colors use ANSI escape codes. These are special character sequences that tell your terminal to switch text color. You will define a color palette, a function to detect whether color is supported, and the display function that renders everything.

  • In subnetcalc.py, add the color support section between the imports and the # --- Subnet Calculation --- comment. Paste this code after the blank line below import sys:
# --- Color Support ---

COLORS = {
    "header": "\033[1;36m",
    "label": "\033[1;33m",
    "value": "\033[0;37m",
    "network_bits": "\033[1;32m",
    "host_bits": "\033[1;31m",
    "reset": "\033[0m",
}

NO_COLORS = {key: "" for key in COLORS}


def should_use_color(no_color_flag):
    """Determine if color output should be used."""
    if no_color_flag:
        return False
    if os.environ.get("NO_COLOR") is not None:
        return False
    if not sys.stdout.isatty():
        return False
    return True

What does this code do?

  • COLORS maps friendly names to ANSI escape sequences. For example, \033[1;32m means bold green (for network bits) and \033[1;31m means bold red (for host bits).
  • NO_COLORS is the same dictionary shape but with empty strings. This lets your display code work identically with or without color. No conditional logic needed inside the display function.
  • should_use_color checks three conditions: an explicit --no-color flag, the NO_COLOR environment variable (a community convention), and whether stdout is a terminal (not a pipe).
  • Save subnetcalc.py.

The color infrastructure is ready. Now add the function that uses it to render results.

  • Add the display_results function below get_binary_breakdown and above the existing def main(): function. Paste this code:
# --- Display Functions ---

def display_results(results, binary, vlsm_table, colors):
    """Display formatted subnet calculation results."""
    c = colors

    print(f"\n{c['header']}{'=' * 60}")
    print(f"  SUBNET CALCULATION RESULTS")
    print(f"{'=' * 60}{c['reset']}\n")

    fields = [
        ("Network Address:", results["network_address"]),
        ("Broadcast Address:", results["broadcast_address"]),
        ("Subnet Mask:", results["subnet_mask"]),
        ("Wildcard Mask:", results["wildcard_mask"]),
        ("Prefix Length:", f"/{results['prefix_length']}"),
        ("Usable Host Range:", f"{results['first_host']} - {results['last_host']}"),
        ("Usable Hosts:", str(results["usable_hosts"])),
        ("Total Addresses:", str(results["total_addresses"])),
    ]

    for label, value in fields:
        print(f"  {c['label']}{label:<20}{c['reset']}{c['value']}{value}{c['reset']}")

    # Binary breakdown
    print(f"\n{c['header']}{'\u2500' * 60}")
    print(f"  BINARY BREAKDOWN")
    print(f"{'\u2500' * 60}{c['reset']}\n")

    prefix_len = binary["prefix_length"]
    display_line = "  "
    bit_index = 0

    for i, octet in enumerate(binary["binary_octets"]):
        for bit in octet:
            if bit_index < prefix_len:
                display_line += f"{c['network_bits']}{bit}{c['reset']}"
            else:
                display_line += f"{c['host_bits']}{bit}{c['reset']}"
            bit_index += 1
        if i < 3:
            display_line += "."

    print(display_line)
    print(f"  {c['network_bits']}N = Network bits ({prefix_len}){c['reset']}  "
          f"{c['host_bits']}H = Host bits ({32 - prefix_len}){c['reset']}")

    print()

What does this code do?

  • The function prints a header section with all subnet details (network address, broadcast, masks, host range) using fixed-width label formatting.
  • The binary breakdown iterates bit by bit through all 32 bits. Each bit gets colored green (network bit) or red (host bit) based on whether its position is before or after the prefix length.
  • The legend at the bottom tells the reader how many bits belong to each category. The vlsm_table parameter is for a feature you will build in the next step. For now, you will pass an empty list.
  • Save subnetcalc.py.

One last piece. You need to update main() to wire everything together.

  • Find the existing def main(): function at the bottom of the file (above if __name__ == "__main__":).
  • Replace the entire main() function with this updated version:
# --- CLI ---

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    parser.add_argument(
        "--no-color",
        action="store_true",
        help="Disable colored output"
    )

    args = parser.parse_args()

    use_color = should_use_color(args.no_color)
    colors = COLORS if use_color else NO_COLORS

    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    display_results(results, binary, [], colors)

What's happening here?

  • The new --no-color flag gives users an explicit way to disable color output.
  • should_use_color() decides whether to use the COLORS or NO_COLORS dictionary. Both have the same keys, so the display function works identically either way.
  • The try/except block catches invalid inputs (like not_an_ip) and prints a helpful error to stderr instead of crashing with a traceback.
  • display_results(results, binary, [], colors) passes an empty list for the VLSM table parameter. You will populate that in the next step.
  • Save subnetcalc.py.
  • Run the calculator by running this command:
python3 subnetcalc.py 192.168.1.0/24

You should see a full subnet breakdown with colored output. Network bits appear in green and host bits appear in red in the binary section.

Seeing a syntax error or no color?

  • If you get IndentationError, check that each function is indented consistently with 4 spaces (not tabs mixed with spaces).
  • If you see raw escape codes like \033[1;36m in your output, your terminal may not support ANSI colors. Try running with --no-color to confirm the tool works, or use the VS Code integrated terminal which supports color.
  • If you get NameError: name 'ipaddress' is not defined, make sure import ipaddress appears near the top of the file below import argparse.

Help me debug my subnet calculator output

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the complete subnetcalc.py file as it should look at this point:

#!/usr/bin/env python3
"""Subnet Calculator CLI Tool."""

import argparse
import ipaddress
import os
import sys


# --- Color Support ---

COLORS = {
    "header": "\033[1;36m",
    "label": "\033[1;33m",
    "value": "\033[0;37m",
    "network_bits": "\033[1;32m",
    "host_bits": "\033[1;31m",
    "reset": "\033[0m",
}

NO_COLORS = {key: "" for key in COLORS}


def should_use_color(no_color_flag):
    """Determine if color output should be used."""
    if no_color_flag:
        return False
    if os.environ.get("NO_COLOR") is not None:
        return False
    if not sys.stdout.isatty():
        return False
    return True


# --- Subnet Calculation ---

def calculate_subnet(cidr_input):
    """Calculate all subnet details from a CIDR string."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    hosts_list = list(network.hosts())
    num_hosts = len(hosts_list)

    if num_hosts > 0:
        first_host = str(hosts_list[0])
        last_host = str(hosts_list[-1])
    else:
        first_host = "N/A"
        last_host = "N/A"

    return {
        "network_address": str(network.network_address),
        "broadcast_address": str(network.broadcast_address),
        "subnet_mask": str(network.netmask),
        "wildcard_mask": str(network.hostmask),
        "prefix_length": network.prefixlen,
        "first_host": first_host,
        "last_host": last_host,
        "usable_hosts": num_hosts,
        "total_addresses": network.num_addresses,
    }


def get_binary_breakdown(cidr_input):
    """Generate binary representation showing network vs host bits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    addr_int = int(network.network_address)
    binary_str = format(addr_int, "032b")

    octets = [binary_str[i:i + 8] for i in range(0, 32, 8)]

    return {
        "binary_octets": octets,
        "prefix_length": prefix_len,
        "network_bits": binary_str[:prefix_len],
        "host_bits": binary_str[prefix_len:],
    }


# --- Display Functions ---

def display_results(results, binary, vlsm_table, colors):
    """Display formatted subnet calculation results."""
    c = colors

    print(f"\n{c['header']}{'=' * 60}")
    print(f"  SUBNET CALCULATION RESULTS")
    print(f"{'=' * 60}{c['reset']}\n")

    fields = [
        ("Network Address:", results["network_address"]),
        ("Broadcast Address:", results["broadcast_address"]),
        ("Subnet Mask:", results["subnet_mask"]),
        ("Wildcard Mask:", results["wildcard_mask"]),
        ("Prefix Length:", f"/{results['prefix_length']}"),
        ("Usable Host Range:", f"{results['first_host']} - {results['last_host']}"),
        ("Usable Hosts:", str(results["usable_hosts"])),
        ("Total Addresses:", str(results["total_addresses"])),
    ]

    for label, value in fields:
        print(f"  {c['label']}{label:<20}{c['reset']}{c['value']}{value}{c['reset']}")

    # Binary breakdown
    print(f"\n{c['header']}{'\u2500' * 60}")
    print(f"  BINARY BREAKDOWN")
    print(f"{'\u2500' * 60}{c['reset']}\n")

    prefix_len = binary["prefix_length"]
    display_line = "  "
    bit_index = 0

    for i, octet in enumerate(binary["binary_octets"]):
        for bit in octet:
            if bit_index < prefix_len:
                display_line += f"{c['network_bits']}{bit}{c['reset']}"
            else:
                display_line += f"{c['host_bits']}{bit}{c['reset']}"
            bit_index += 1
        if i < 3:
            display_line += "."

    print(display_line)
    print(f"  {c['network_bits']}N = Network bits ({prefix_len}){c['reset']}  "
          f"{c['host_bits']}H = Host bits ({32 - prefix_len}){c['reset']}")

    print()


# --- CLI ---

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    parser.add_argument(
        "--no-color",
        action="store_true",
        help="Disable colored output"
    )

    args = parser.parse_args()

    use_color = should_use_color(args.no_color)
    colors = COLORS if use_color else NO_COLORS

    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    display_results(results, binary, [], colors)


if __name__ == "__main__":
    main()

Your calculation engine is working and the binary visualization makes subnet masks tangible. Next up, you will add a VLSM subdivision table that shows how a network can be split into smaller subnets.

Add the VLSM Subdivision Table

Your calculator now shows subnet details and a color-coded binary breakdown. That answers "what is this network?" but not "how can I split it?"

In this step, you will build a VLSM (Variable Length Subnet Masking) subdivision table that shows every possible way to split a network into smaller subnets. This is the same planning tool network engineers use when designing address allocations.

In this step, get ready to:
  • Build a function that calculates all possible subnet splits for a given network.
  • Display the VLSM table with formatted columns and ANSI colors.
  • Verify the math by testing with different prefix lengths.
Build the get_vlsm_table function

When you have a /24 network, you might want to know: "How many /26 subnets can I carve out of this, and how many hosts does each one support?" The get_vlsm_table function answers this for every valid split level.

  • In subnetcalc.py, find the get_binary_breakdown function and add the following new function directly below it:
def get_vlsm_table(cidr_input):
    """Generate VLSM subdivision table showing possible subnet splits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    subnets = []
    max_prefix = min(prefix_len + 7, 31)

    for new_prefix in range(prefix_len + 1, max_prefix):
        hosts_per_subnet = 2 ** (32 - new_prefix) - 2

        if hosts_per_subnet < 2:
            break

        num_subnets = 2 ** (new_prefix - prefix_len)

        subnets.append({
            "prefix": f"/{new_prefix}",
            "num_subnets": num_subnets,
            "hosts_per_subnet": hosts_per_subnet,
            "subnet_mask": str(ipaddress.IPv4Network(f"0.0.0.0/{new_prefix}").netmask),
        })

    return subnets

What does this code do?

This function iterates through progressively smaller subnet sizes, starting one prefix bit longer than the input network.

  • max_prefix = min(prefix_len + 7, 31) caps how far down the table goes. You never exceed /30 (which gives 2 usable hosts).
  • hosts_per_subnet = 2 ** (32 - new_prefix) - 2 calculates usable hosts. The - 2 accounts for the network address and broadcast address that every subnet reserves.
  • num_subnets = 2 ** (new_prefix - prefix_len) tells you how many subnets of that size fit in the original network. Each extra prefix bit doubles the count.
  • The loop breaks early if hosts_per_subnet < 2, since a subnet with fewer than 2 usable hosts has no practical value for host assignment.
  • Save subnetcalc.py.

You will not see a change yet. The next chunk wires this function into the display output.

What is VLSM?

Variable Length Subnet Masking lets you divide a network into subnets of different sizes instead of forcing every subnet to be the same size. A /24 network could be split into one /25 (126 hosts) and two /26s (62 hosts each) to match actual department sizes.

The table you are building shows all the uniform split options. The Secret Mission later extends this into a planner that allocates mixed-size subnets.

Display the VLSM table after the binary breakdown

The display_results function already accepts a vlsm_table parameter. Now you will add the rendering logic that formats the table data into aligned columns with ANSI colors.

  • In the display_results function, find this line near the end:
    print()
  • Add the following code directly above that print() line:
    # VLSM table
    if vlsm_table:
        print(f"\n{c['header']}{'─' * 60}")
        print(f"  VLSM SUBDIVISION TABLE")
        print(f"{'─' * 60}{c['reset']}\n")

        print(f"  {c['label']}{'Prefix':<10}{'Subnets':<12}{'Hosts/Subnet':<16}{'Mask'}{c['reset']}")
        print(f"  {'─' * 54}")

        for entry in vlsm_table:
            print(f"  {c['value']}{entry['prefix']:<10}"
                  f"{entry['num_subnets']:<12}"
                  f"{entry['hosts_per_subnet']:<16}"
                  f"{entry['subnet_mask']}{c['reset']}")

What does this code do?

  • The if vlsm_table: guard skips the section entirely when there are no valid splits (e.g., a /31 or /32 network).
  • The :<10, :<12, and :<16 format specifiers left-align each column to a fixed width. This keeps rows neat regardless of how many digits the values have.
  • Colors follow the same scheme as the rest of the output: header color for the section title, label color for column headers, and value color for data rows.
  • Save subnetcalc.py.

You will not see a change yet. The final wiring in main() needs to call get_vlsm_table() and pass real data to the display function.

Wire up main() and test with different prefix lengths

Right now main() passes an empty list to display_results. You need to call the new function and pass its result instead.

  • In the main() function, find the try: block. It currently looks like this:
    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    display_results(results, binary, [], colors)
  • Add vlsm_table = get_vlsm_table(args.network) as the third line inside the try: block, and replace [] with vlsm_table in the display call. The result should look like this:
    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
        vlsm_table = get_vlsm_table(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    display_results(results, binary, vlsm_table, colors)

What changed?

You added one line to compute the VLSM table and swapped the empty list placeholder for the real data. Both calls sit inside the same try/except block so any invalid input is caught in one place.

  • Save subnetcalc.py.
  • Test with a /24 network by running:
python3 subnetcalc.py 192.168.1.0/24

You should see the VLSM SUBDIVISION TABLE section appear below the binary breakdown. The table shows splits from /25 through /30.

  • Verify the math: the /25 row should show 2 subnets with 126 hosts each. The /26 row should show 4 subnets with 62 hosts each.
  • Test with a /16 network by running:
python3 subnetcalc.py 10.0.0.0/16

You should see entries from /17 through /22. The /17 row shows 2 subnets with 32766 hosts each.

  • Test with a /28 network by running:
python3 subnetcalc.py 192.168.1.0/28

You should see only /29 (2 subnets, 6 hosts each) and /30 (4 subnets, 2 hosts each). The table stops there because smaller subnets have fewer than 2 usable hosts.

Not seeing the VLSM table?

  • Check that the VLSM table code block is inside the display_results function and positioned before the final print() call.
  • Confirm that main() now passes vlsm_table instead of [] to display_results. An empty list evaluates as falsy, so the if vlsm_table: guard skips rendering entirely.
  • Make sure get_vlsm_table is defined before display_results in the file. Python reads top-to-bottom, and functions called inside other functions must be defined by the time they run.

Help me debug my VLSM table output

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full subnetcalc.py as it should look at this point:

#!/usr/bin/env python3
"""Subnet Calculator CLI Tool."""

import argparse
import ipaddress
import os
import sys


# --- Color Support ---

COLORS = {
    "header": "\033[1;36m",
    "label": "\033[1;33m",
    "value": "\033[0;37m",
    "network_bits": "\033[1;32m",
    "host_bits": "\033[1;31m",
    "reset": "\033[0m",
}

NO_COLORS = {key: "" for key in COLORS}


def should_use_color(no_color_flag):
    """Determine if color output should be used."""
    if no_color_flag:
        return False
    if os.environ.get("NO_COLOR") is not None:
        return False
    if not sys.stdout.isatty():
        return False
    return True


# --- Subnet Calculation ---

def calculate_subnet(cidr_input):
    """Calculate all subnet details from a CIDR string."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    hosts_list = list(network.hosts())
    num_hosts = len(hosts_list)

    if num_hosts > 0:
        first_host = str(hosts_list[0])
        last_host = str(hosts_list[-1])
    else:
        first_host = "N/A"
        last_host = "N/A"

    return {
        "network_address": str(network.network_address),
        "broadcast_address": str(network.broadcast_address),
        "subnet_mask": str(network.netmask),
        "wildcard_mask": str(network.hostmask),
        "prefix_length": network.prefixlen,
        "first_host": first_host,
        "last_host": last_host,
        "usable_hosts": num_hosts,
        "total_addresses": network.num_addresses,
    }


def get_binary_breakdown(cidr_input):
    """Generate binary representation showing network vs host bits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    addr_int = int(network.network_address)
    binary_str = format(addr_int, "032b")

    octets = [binary_str[i:i + 8] for i in range(0, 32, 8)]

    return {
        "binary_octets": octets,
        "prefix_length": prefix_len,
        "network_bits": binary_str[:prefix_len],
        "host_bits": binary_str[prefix_len:],
    }


def get_vlsm_table(cidr_input):
    """Generate VLSM subdivision table showing possible subnet splits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    subnets = []
    max_prefix = min(prefix_len + 7, 31)

    for new_prefix in range(prefix_len + 1, max_prefix):
        hosts_per_subnet = 2 ** (32 - new_prefix) - 2

        if hosts_per_subnet < 2:
            break

        num_subnets = 2 ** (new_prefix - prefix_len)

        subnets.append({
            "prefix": f"/{new_prefix}",
            "num_subnets": num_subnets,
            "hosts_per_subnet": hosts_per_subnet,
            "subnet_mask": str(ipaddress.IPv4Network(f"0.0.0.0/{new_prefix}").netmask),
        })

    return subnets


# --- Display Functions ---

def display_results(results, binary, vlsm_table, colors):
    """Display formatted subnet calculation results."""
    c = colors

    print(f"\n{c['header']}{'=' * 60}")
    print(f"  SUBNET CALCULATION RESULTS")
    print(f"{'=' * 60}{c['reset']}\n")

    fields = [
        ("Network Address:", results["network_address"]),
        ("Broadcast Address:", results["broadcast_address"]),
        ("Subnet Mask:", results["subnet_mask"]),
        ("Wildcard Mask:", results["wildcard_mask"]),
        ("Prefix Length:", f"/{results['prefix_length']}"),
        ("Usable Host Range:", f"{results['first_host']} - {results['last_host']}"),
        ("Usable Hosts:", str(results["usable_hosts"])),
        ("Total Addresses:", str(results["total_addresses"])),
    ]

    for label, value in fields:
        print(f"  {c['label']}{label:<20}{c['reset']}{c['value']}{value}{c['reset']}")

    # Binary breakdown
    print(f"\n{c['header']}{'─' * 60}")
    print(f"  BINARY BREAKDOWN")
    print(f"{'─' * 60}{c['reset']}\n")

    prefix_len = binary["prefix_length"]
    display_line = "  "
    bit_index = 0

    for i, octet in enumerate(binary["binary_octets"]):
        for bit in octet:
            if bit_index < prefix_len:
                display_line += f"{c['network_bits']}{bit}{c['reset']}"
            else:
                display_line += f"{c['host_bits']}{bit}{c['reset']}"
            bit_index += 1
        if i < 3:
            display_line += "."

    print(display_line)
    print(f"  {c['network_bits']}N = Network bits ({prefix_len}){c['reset']}  "
          f"{c['host_bits']}H = Host bits ({32 - prefix_len}){c['reset']}")

    # VLSM table
    if vlsm_table:
        print(f"\n{c['header']}{'─' * 60}")
        print(f"  VLSM SUBDIVISION TABLE")
        print(f"{'─' * 60}{c['reset']}\n")

        print(f"  {c['label']}{'Prefix':<10}{'Subnets':<12}{'Hosts/Subnet':<16}{'Mask'}{c['reset']}")
        print(f"  {'─' * 54}")

        for entry in vlsm_table:
            print(f"  {c['value']}{entry['prefix']:<10}"
                  f"{entry['num_subnets']:<12}"
                  f"{entry['hosts_per_subnet']:<16}"
                  f"{entry['subnet_mask']}{c['reset']}")

    print()


# --- CLI ---

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    parser.add_argument(
        "--no-color",
        action="store_true",
        help="Disable colored output"
    )

    args = parser.parse_args()

    use_color = should_use_color(args.no_color)
    colors = COLORS if use_color else NO_COLORS

    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
        vlsm_table = get_vlsm_table(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    display_results(results, binary, vlsm_table, colors)


if __name__ == "__main__":
    main()

Your calculator now shows the full picture: subnet details, a binary bit map, and a VLSM subdivision table. Next up, you will add CLI flags for JSON export and automatic color control so the tool behaves properly when piped or scripted.

Add CLI Flags, Color Control, and JSON Export

Your subnet calculator already computes results, visualizes binary breakdowns, and renders a VLSM table. Right now it always works the same way regardless of how it's invoked.

A production-quality CLI tool adapts to its environment. In this step, you'll verify that color degrades cleanly when piped or disabled, add a --export flag for JSON output, and confirm that invalid inputs produce helpful error messages.

In this step, get ready to:
  • Verify color control with the --no-color flag and TTY detection.
  • Add a --export flag that dumps results to a JSON file.
  • Test input validation and error handling with invalid networks.
Verify the --no-color flag and TTY detection

Your tool already has a should_use_color function that checks three conditions before using color. This is a three-layer approach matching how real CLI tools like git and ls handle color.

  • Test the --no-color flag by running this command:
python3 subnetcalc.py 192.168.1.0/24 --no-color

You should see the same subnet results, but without any colored text. All the escape sequences are suppressed.

  • Test TTY detection by piping the output through cat:
python3 subnetcalc.py 192.168.1.0/24 | cat

The output appears without color codes even though you did not pass --no-color. When stdout is piped, sys.stdout.isatty() returns False and your tool auto-disables color.

What is the NO_COLOR convention?

Your should_use_color function also checks for a NO_COLOR environment variable. This is a community standard from no-color.org. When present and not empty, CLI tools should suppress color output.

Users can set NO_COLOR=1 globally in their shell profile to disable color across all supporting tools at once.

Add the --export flag for JSON output

CLI tools that produce structured data should offer a machine-readable export option. You'll add an --export flag that serializes all results to a JSON file.

  • In subnetcalc.py, find the import section at the top of the file. Add import json between import ipaddress and import os:
import json
  • Now add the export_json function. Scroll down to just after the closing print() at the end of display_results and add this function above the # --- CLI --- comment:
def export_json(results, binary, vlsm_table, filepath):
    """Export results to a JSON file."""
    output = {
        "subnet_info": results,
        "binary_breakdown": {
            "octets": binary["binary_octets"],
            "prefix_length": binary["prefix_length"],
            "network_bits": binary["network_bits"],
            "host_bits": binary["host_bits"],
        },
        "vlsm_table": vlsm_table,
    }

    with open(filepath, "w") as f:
        json.dump(output, f, indent=2)

    print(f"Results exported to {filepath}", file=sys.stderr)

What does this code do?

  • The function builds an output dictionary with three top-level keys: subnet_info, binary_breakdown, and vlsm_table.
  • json.dump(output, f, indent=2) writes the dictionary to the file as nicely-formatted JSON with 2-space indentation.
  • The confirmation message prints to stderr so it does not interfere with piped stdout output.
  • Next, wire up the --export argument. In your main() function, find the parser.add_argument block for --no-color. Add this new argument directly above it:
    parser.add_argument(
        "--export",
        metavar="FILE",
        help="Export results to a JSON file"
    )
  • Finally, add the export call at the end of main(). Find the line display_results(results, binary, vlsm_table, colors) and add this block below it:
    # Export if requested
    if args.export:
        export_json(results, binary, vlsm_table, args.export)

Why print to stderr?

The confirmation message "Results exported to..." goes to stderr so that if the learner pipes the main output somewhere, the file-write confirmation does not contaminate the data stream.

  • Save subnetcalc.py.
  • Test the export by running:
python3 subnetcalc.py 192.168.1.0/24 --export results.json

You should see the normal colored output in your terminal plus a message on stderr saying Results exported to results.json.

  • Verify the JSON file was created by running:
cat results.json

You should see a well-formatted JSON object with subnet_info, binary_breakdown, and vlsm_table keys containing all your calculation data.

Seeing a NameError for json?

Make sure you added import json at the top of the file, between import ipaddress and import os. Without it, Python cannot find the json module when export_json tries to call json.dump().

Help me debug my JSON export

Test error handling with invalid inputs

Your main() function already wraps the calculation calls in a try/except ValueError block. When ipaddress.ip_network() receives invalid input, it raises a ValueError that your tool catches and reports to stderr.

  • Test with a non-IP string by running:
python3 subnetcalc.py not_an_ip

You should see an error message starting with Error: Invalid network input printed to stderr. The script exits immediately without printing any subnet results.

  • Test with an invalid prefix length by running:
python3 subnetcalc.py 192.168.1.0/33
  • Test with impossible octets by running:
python3 subnetcalc.py 999.999.999.999/24

Both produce the same helpful error pattern. The tool exits with code 1, which signals failure to any script or pipeline that calls it.

Why exit with code 1?

Exit code 0 means success. Any non-zero code means failure. By calling sys.exit(1) on bad input, your tool plays nicely with shell scripts and automation that check $? after each command.

✔️ Awesome, I've got everything!

Great. Double check you've saved subnetcalc.py and confirmed both --export and --no-color work as expected.

ⓧ I'd like to double check the full code

#!/usr/bin/env python3
"""Subnet Calculator CLI Tool."""

import argparse
import ipaddress
import json
import os
import sys


# --- Color Support ---

COLORS = {
    "header": "\033[1;36m",
    "label": "\033[1;33m",
    "value": "\033[0;37m",
    "network_bits": "\033[1;32m",
    "host_bits": "\033[1;31m",
    "reset": "\033[0m",
}

NO_COLORS = {key: "" for key in COLORS}


def should_use_color(no_color_flag):
    """Determine if color output should be used."""
    if no_color_flag:
        return False
    if os.environ.get("NO_COLOR") is not None:
        return False
    if not sys.stdout.isatty():
        return False
    return True


# --- Subnet Calculation ---

def calculate_subnet(cidr_input):
    """Calculate all subnet details from a CIDR string."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    hosts_list = list(network.hosts())
    num_hosts = len(hosts_list)

    if num_hosts > 0:
        first_host = str(hosts_list[0])
        last_host = str(hosts_list[-1])
    else:
        first_host = "N/A"
        last_host = "N/A"

    return {
        "network_address": str(network.network_address),
        "broadcast_address": str(network.broadcast_address),
        "subnet_mask": str(network.netmask),
        "wildcard_mask": str(network.hostmask),
        "prefix_length": network.prefixlen,
        "first_host": first_host,
        "last_host": last_host,
        "usable_hosts": num_hosts,
        "total_addresses": network.num_addresses,
    }


def get_binary_breakdown(cidr_input):
    """Generate binary representation showing network vs host bits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    addr_int = int(network.network_address)
    binary_str = format(addr_int, "032b")

    octets = [binary_str[i:i + 8] for i in range(0, 32, 8)]

    return {
        "binary_octets": octets,
        "prefix_length": prefix_len,
        "network_bits": binary_str[:prefix_len],
        "host_bits": binary_str[prefix_len:],
    }


def get_vlsm_table(cidr_input):
    """Generate VLSM subdivision table showing possible subnet splits."""
    network = ipaddress.ip_network(cidr_input, strict=False)
    prefix_len = network.prefixlen

    subnets = []
    max_prefix = min(prefix_len + 7, 31)

    for new_prefix in range(prefix_len + 1, max_prefix):
        hosts_per_subnet = 2 ** (32 - new_prefix) - 2

        if hosts_per_subnet < 2:
            break

        num_subnets = 2 ** (new_prefix - prefix_len)

        subnets.append({
            "prefix": f"/{new_prefix}",
            "num_subnets": num_subnets,
            "hosts_per_subnet": hosts_per_subnet,
            "subnet_mask": str(ipaddress.IPv4Network(f"0.0.0.0/{new_prefix}").netmask),
        })

    return subnets


# --- Display Functions ---

def display_results(results, binary, vlsm_table, colors):
    """Display formatted subnet calculation results."""
    c = colors

    print(f"\n{c['header']}{'=' * 60}")
    print(f"  SUBNET CALCULATION RESULTS")
    print(f"{'=' * 60}{c['reset']}\n")

    fields = [
        ("Network Address:", results["network_address"]),
        ("Broadcast Address:", results["broadcast_address"]),
        ("Subnet Mask:", results["subnet_mask"]),
        ("Wildcard Mask:", results["wildcard_mask"]),
        ("Prefix Length:", f"/{results['prefix_length']}"),
        ("Usable Host Range:", f"{results['first_host']} - {results['last_host']}"),
        ("Usable Hosts:", str(results["usable_hosts"])),
        ("Total Addresses:", str(results["total_addresses"])),
    ]

    for label, value in fields:
        print(f"  {c['label']}{label:<20}{c['reset']}{c['value']}{value}{c['reset']}")

    # Binary breakdown
    print(f"\n{c['header']}{'\u2500' * 60}")
    print(f"  BINARY BREAKDOWN")
    print(f"{'\u2500' * 60}{c['reset']}\n")

    prefix_len = binary["prefix_length"]
    display_line = "  "
    bit_index = 0

    for i, octet in enumerate(binary["binary_octets"]):
        for bit in octet:
            if bit_index < prefix_len:
                display_line += f"{c['network_bits']}{bit}{c['reset']}"
            else:
                display_line += f"{c['host_bits']}{bit}{c['reset']}"
            bit_index += 1
        if i < 3:
            display_line += "."

    print(display_line)
    print(f"  {c['network_bits']}N = Network bits ({prefix_len}){c['reset']}  "
          f"{c['host_bits']}H = Host bits ({32 - prefix_len}){c['reset']}")

    # VLSM table
    if vlsm_table:
        print(f"\n{c['header']}{'\u2500' * 60}")
        print(f"  VLSM SUBDIVISION TABLE")
        print(f"{'\u2500' * 60}{c['reset']}\n")

        print(f"  {c['label']}{'Prefix':<10}{'Subnets':<12}{'Hosts/Subnet':<16}{'Mask'}{c['reset']}")
        print(f"  {'\u2500' * 54}")

        for entry in vlsm_table:
            print(f"  {c['value']}{entry['prefix']:<10}"
                  f"{entry['num_subnets']:<12}"
                  f"{entry['hosts_per_subnet']:<16}"
                  f"{entry['subnet_mask']}{c['reset']}")

    print()


def export_json(results, binary, vlsm_table, filepath):
    """Export results to a JSON file."""
    output = {
        "subnet_info": results,
        "binary_breakdown": {
            "octets": binary["binary_octets"],
            "prefix_length": binary["prefix_length"],
            "network_bits": binary["network_bits"],
            "host_bits": binary["host_bits"],
        },
        "vlsm_table": vlsm_table,
    }

    with open(filepath, "w") as f:
        json.dump(output, f, indent=2)

    print(f"Results exported to {filepath}", file=sys.stderr)


# --- CLI ---

def main():
    parser = argparse.ArgumentParser(
        description="Subnet Calculator - Compute network details from IP/CIDR notation"
    )
    parser.add_argument(
        "network",
        help="IP address in CIDR notation (e.g., 192.168.1.0/24)"
    )
    parser.add_argument(
        "--export",
        metavar="FILE",
        help="Export results to a JSON file"
    )
    parser.add_argument(
        "--no-color",
        action="store_true",
        help="Disable colored output"
    )

    args = parser.parse_args()

    # Determine color usage
    use_color = should_use_color(args.no_color)
    colors = COLORS if use_color else NO_COLORS

    # Validate and calculate
    try:
        results = calculate_subnet(args.network)
        binary = get_binary_breakdown(args.network)
        vlsm_table = get_vlsm_table(args.network)
    except ValueError as e:
        print(f"Error: Invalid network input - {e}", file=sys.stderr)
        sys.exit(1)

    # Display
    display_results(results, binary, vlsm_table, colors)

    # Export if requested
    if args.export:
        export_json(results, binary, vlsm_table, args.export)


if __name__ == "__main__":
    main()

Your subnet calculator is now a complete, production-ready CLI tool. It computes, visualizes, exports, and handles errors gracefully. That is the kind of utility that proves you understand networking concepts well enough to build real tools around them.

Secret mission

VLSM Planner Mode

Your subnet calculator shows all possible splits, but what if you need specific subnet sizes? Add a --plan flag that accepts comma-separated host requirements and outputs an optimal VLSM allocation plan using a greedy largest-first algorithm.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with zero external dependencies, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your project files on your Desktop or remove them entirely.

Resources you used:

  • The subnet-calculator folder on your Desktop (contains subnetcalc.py and any exported JSON files).

Keep everything running

No action needed. Choose this if you want to keep using the subnet calculator or plan to extend it further.

  • Your subnet-calculator folder stays on your Desktop.
  • You can run python3 subnetcalc.py anytime from inside that folder.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files sit on disk and use minimal storage.

  • Leave the subnet-calculator folder in place. It takes up less than a few kilobytes.
  • When you come back, open the folder in VS Code and pick up where you left off.

Delete - I don't want to use this again

Remove all project files from your machine.

  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/subnet-calculator
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\subnet-calculator
  • Confirm the folder is gone by checking your Desktop. You should no longer see a subnet-calculator folder.

Nice Work!

Nice Work!

You just built a complete subnet calculator CLI tool from scratch using nothing but Python's standard library. This is the kind of practical utility that proves you understand subnetting well enough to build tools around it.

You've learned how to:

  • Build a subnet calculator that computes network addresses, broadcast addresses, subnet masks, wildcard masks, and usable host ranges from any CIDR input.
  • Create a color-coded binary visualization using ANSI escape codes that visually separates network bits from host bits, with automatic color degradation when piped or redirected.
  • Design a professional CLI interface with argparse featuring JSON export, TTY detection, and a VLSM subdivision table showing all possible subnet splits.
  • Secret Mission: Implemented a VLSM planner that accepts comma-separated host requirements and allocates optimally-sized subnets using a greedy largest-first algorithm.

Ready to quiz yourself?