Subnetting Trainer — Practice IP Subnetting with Random Exercises

Practice IP Subnetting — Interactive Trainer for Fast, Hands-On Drills

I’m studying networking and I believe the only way to really learn subnetting is lots of practice. Theory is fine, but speed and accuracy come from repetition. I wrote this little app for that exact purpose. This is the fourth tool I’ve built — the earlier ones are bigger and still in progress, so I haven’t published them yet. I made this one to quickly run through examples, see mistakes, and reinforce the concepts.

What it does

  • The app shows a random Host IP and the original subnet mask that IP belongs to (only private ranges are used: 10/8, 172.16/12, 192.168/16).
  • It then gives a stricter new subnet mask (/CIDR) and asks you to calculate and fill in:
    • Subnet bits, Number of subnets, Host bits per subnet, Hosts per subnet
    • Network address, First usable host, Last usable host, Broadcast address
  • Input fields are designed for speed: IPs are split into four octet boxes; typing a dot (.) or entering 3 digits automatically advances to the next box. Pasting a full dotted IP spreads it across the boxes.
  • Press Check to validate your answers — the app tells you which fields are wrong. If everything is correct, a new exercise loads automatically. Use Show solution to fill in the correct values if you’re stuck.

Why I built it

I could follow the theory in class, but I still struggled when speed mattered. For exams or practical tasks, being fast and accurate takes repeated short practice sessions. This app speeds up that cycle: solve one example, check it, see what you got wrong, repeat. I kept the UI minimal so the workflow is quick: fast IP entry, clear error feedback.

Features — details

  • Examples are generated only from private IP ranges (no touching real production IPs).
  • The new mask is always stricter than the original mask to force subnetting practice.
  • Special cases such as /31 and /32 are handled: when there are no usable hosts the First/Last host fields should be left blank and the app accepts that.
  • The app shows exactly which fields are wrong so you know what to fix.
  • The About/Tutorial window contains a worked example (172.22.32.12 /16 → /19) so you can review the reasoning inside the app.
  • Written entirely in Python + Tkinter with no external libraries — you can run from source or package as a single .exe.

How it works (quick flow)

  1. Open the app — the Given block at the top displays the Host IP, Original mask and New mask.
  2. Below, enter your answers: numeric fields and four-octet IP boxes. Octet fields auto-advance as you type.
  3. Press Check to validate. If any answers are wrong the app tells you which ones; if all are correct a new exercise is loaded. Use Show solution to auto-fill the correct values and compare with the tutorial.
  4. Open About/Tutorial to read the worked example and see how the third octet blocks, network and broadcast are calculated.

Who is this for?

  • Networking students (CCNA/Network+ prep)
  • People preparing for exams who need speed drills
  • Instructors who want a quick classroom demo tool
  • Anyone learning IP addressing fundamentals

Distribution & usage

  • I package this as a single .exe (Windows) and upload it to my site. That way friends don’t need Python installed.
  • If you plan to distribute widely, I recommend code-signing the executable and publishing a checksum. For casual sharing, a direct download link is enough.
  • Note: Windows SmartScreen or Defender may warn on unsigned builds. That doesn’t mean the app is malicious — it’s because the publisher isn’t a recognized CA. If you share broadly, consider getting a Code Signing certificate.

Technical notes

  • Written for Python 3.10+ using tkinter, ipaddress, random and standard libs. No extra packages required.
  • Packaging is done with PyInstaller; I included PNG/ICO support so the app has a window icon.
  • Edge cases handled (e.g. /31, /32) — if there are no usable hosts the user should leave First/Last host blank and the app accepts it.
  • Only private IPs are generated to avoid targeting any real public hosts.

Feedback & future plans

If you use it and have suggestions or find bugs, please tell me. Feature ideas I’m considering:

  • Automatic difficulty levels (easy → hard)
  • VLSM practice mode (split subnets, allocate ranges)
  • Exam mode (timed questions)

This tool is intentionally small and focused — my goal was a quick, repeatable practice loop. I’ll add more features over time.

Source code

import random
import tkinter as tk
from tkinter import ttk, messagebox
import ipaddress, re, webbrowser, textwrap, os, sys

APP_TITLE = "Subnetting Trainer"
PADDING = 8

# ---------- random data & helpers ----------
def ip_rand_private():
    which = random.choice(["10", "172", "192"])
    if which == "10":
        ip = ipaddress.IPv4Address(f"10.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}")
        original_prefix = 8
    elif which == "172":
        second = random.randint(16,31)
        ip = ipaddress.IPv4Address(f"172.{second}.{random.randint(0,255)}.{random.randint(1,254)}")
        original_prefix = 16
    else:
        ip = ipaddress.IPv4Address(f"192.168.{random.randint(0,255)}.{random.randint(1,254)}")
        original_prefix = 24
    return ip, original_prefix

def mask_from_prefix(p): return ipaddress.IPv4Network(f"0.0.0.0/{p}").netmask
def dotted(x): return str(x)

# ---------- segmented IP entry ----------
class IPEntry(ttk.Frame):
    def __init__(self, parent, width=3, allow_empty=False, *a, **kw):
        super().__init__(parent, *a, **kw)
        self.allow_empty = allow_empty
        self.vars = [tk.StringVar() for _ in range(4)]
        self.entries = []
        for i in range(4):
            e = ttk.Entry(self, width=width, textvariable=self.vars[i], justify="center")
            e.grid(row=0, column=i*2)
            e.bind("<KeyRelease>", lambda ev, idx=i: self._on_key(ev, idx))
            e.bind("<FocusIn>", lambda ev, idx=i: self.entries[idx].select_range(0, tk.END))
            e.bind("<<Paste>>", self._on_paste); e.bind("<Control-v>", self._on_paste)
            self.entries.append(e)
            if i < 3: ttk.Label(self, text=".", padding=(2,0)).grid(row=0, column=i*2+1, sticky="w")

    def _on_paste(self, event):
        try: clip = self.clipboard_get()
        except Exception: return "break"
        m = re.match(r"^\s*(\d{1,3})(?:\.(\d{1,3})(?:\.(\d{1,3})(?:\.(\d{1,3}))?)?)?\s*$", clip.strip())
        if not m: return "break"
        for i in range(4):
            val = (m.groups()[i] if i < len(m.groups()) and m.groups()[i] is not None else "")
            self.vars[i].set(val)
        return "break"

    def _on_key(self, event, idx):
        s = self.vars[idx].get()
        if event.keysym == "period" or event.char == ".":
            self.vars[idx].set(re.sub(r"\.+$", "", s))
            if idx < 3: self.entries[idx+1].focus_set(); self.entries[idx+1].icursor(tk.END)
            return
        if len(s) >= 3 and idx < 3:
            try: iv = int(s)
            except: return
            if iv > 255: self.vars[idx].set("255"); return
            self.entries[idx+1].focus_set(); self.entries[idx+1].icursor(tk.END); return
        if event.keysym == "BackSpace" and s == "" and idx > 0:
            self.entries[idx-1].focus_set()

    def get(self):
        parts = [v.get().strip() for v in self.vars]
        if all(p == "" for p in parts):
            if self.allow_empty: return None
            raise ValueError("empty")
        for p in parts:
            if p == "": raise ValueError("incomplete")
            if not re.fullmatch(r"\d{1,3}", p): raise ValueError("bad")
            iv = int(p);  assert 0 <= iv <= 255
        return ".".join(parts)

    def set(self, s):
        if not s: [v.set("") for v in self.vars]; return
        ip = ipaddress.IPv4Address(s)
        parts = str(ip).split(".")
        for i in range(4): self.vars[i].set(parts[i])

# ---------- problem ----------
class Problem:
    def __init__(self):
        self.host_ip, self.original_prefix = ip_rand_private()
        self.new_prefix = max(self.original_prefix+1, random.randint(self.original_prefix+1, 30))
        self.original_mask = mask_from_prefix(self.original_prefix)
        self.new_mask = mask_from_prefix(self.new_prefix)
        self.subnet_bits = self.new_prefix - self.original_prefix
        self.num_subnets = 2**self.subnet_bits
        self.host_bits = 32 - self.new_prefix
        self.hosts_per_subnet = (2**self.host_bits - 2) if self.host_bits > 0 else 0
        net = ipaddress.IPv4Network(f"{self.host_ip}/{self.new_prefix}", strict=False)
        self.network, self.broadcast = net.network_address, net.broadcast_address
        if self.hosts_per_subnet >= 2: self.first_host, self.last_host = self.network+1, self.broadcast-1
        else: self.first_host, self.last_host = None, None

    def solution(self):
        return {
            "Subnet bits": str(self.subnet_bits),
            "Number of subnets": str(self.num_subnets),
            "Host bits per subnet": str(self.host_bits),
            "Hosts per subnet": str(self.hosts_per_subnet),
            "Network address": dotted(self.network),
            "First host": "" if self.first_host is None else dotted(self.first_host),
            "Last host": "" if self.last_host is None else dotted(self.last_host),
            "Broadcast address": dotted(self.broadcast),
        }

# ---------- app ----------
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.resizable(False, False)

        # OPTIONAL: custom window icon (PNG) if placed beside the script as 'app_icon.png'
        try:
            icon_path = resource_path("app_icon.png")
            if os.path.exists(icon_path):
                self.iconphoto(False, tk.PhotoImage(file=icon_path))
        except Exception:
            pass

        self.problem = None
        self._build_ui()
        self.new_problem()

    def _build_ui(self):
        frm = ttk.Frame(self, padding=PADDING); frm.grid(row=0, column=0)

        given = ttk.LabelFrame(frm, text="Given"); given.grid(row=0, column=0, sticky="ew", pady=(0,PADDING))
        self.lbl_hostip = self._row(given, 0, "Host IP address:")
        self.lbl_origmask = self._row(given, 1, "Original subnet mask:")
        self.lbl_newmask = self._row(given, 2, "New subnet mask:")
        self.lbl_newcidr = self._row(given, 3, "New prefix length (/CIDR):")

        q = ttk.LabelFrame(frm, text="Your answers"); q.grid(row=1, column=0, sticky="ew", pady=(0,PADDING))
        self.inputs = {}; r = 0
        for label in ["Subnet bits","Number of subnets","Host bits per subnet","Hosts per subnet"]:
            ttk.Label(q, text=label + ":").grid(row=r, column=0, sticky="w", padx=(PADDING,PADDING), pady=2)
            ent = ttk.Entry(q, width=20); ent.grid(row=r, column=1, sticky="w", padx=(0,PADDING), pady=2)
            self.inputs[label] = ent; r += 1

        for name, allow_empty in [("Network address", False),("First host", True),("Last host", True),("Broadcast address", False)]:
            ttk.Label(q, text=name + ":").grid(row=r, column=0, sticky="w", padx=(PADDING,PADDING), pady=2)
            ipent = IPEntry(q, allow_empty=allow_empty); ipent.grid(row=r, column=1, sticky="w", padx=(0,PADDING), pady=2)
            self.inputs[name] = ipent; r += 1

        btns = ttk.Frame(frm); btns.grid(row=2, column=0, sticky="ew")
        ttk.Button(btns, text="Check", command=self.check_answers).grid(row=0, column=0, padx=(0,PADDING), pady=(0,PADDING))
        ttk.Button(btns, text="Show solution", command=self.show_solution).grid(row=0, column=1, padx=(0,PADDING), pady=(0,PADDING))
        ttk.Button(btns, text="New exercise", command=self.new_problem).grid(row=0, column=2, padx=(0,PADDING), pady=(0,PADDING))
        ttk.Button(btns, text="Help / Tutorial", command=self.show_tutorial).grid(row=0, column=3, padx=(0,PADDING), pady=(0,PADDING))

        self.status = ttk.Label(frm, text="", foreground="green"); self.status.grid(row=3, column=0, sticky="ew")

    def _row(self, parent, r, key):
        ttk.Label(parent, text=key).grid(row=r, column=0, sticky="w", padx=(PADDING,PADDING), pady=2)
        var = tk.StringVar(value="-")
        ttk.Label(parent, textvariable=var, font=("TkDefaultFont",10,"bold")).grid(row=r, column=1, sticky="w", padx=(0,PADDING), pady=2)
        return var

    # -------- tutorial window (wider + example) --------
    def show_tutorial(self):
        top = tk.Toplevel(self); top.title("Subnetting Tutorial")
        top.geometry("820x660")    # wide & tall
        top.resizable(True, True)

        # toolbar
        tb = ttk.Frame(top, padding=(8,6)); tb.pack(side="top", fill="x")
        ttk.Button(tb, text="Visit terminalnotes.com", command=lambda: webbrowser.open("https://terminalnotes.com/")).pack(side="left")
        ttk.Label(tb, text="Read this once, then practice below.", foreground="#444").pack(side="right")

        # scrollable text
        container = ttk.Frame(top); container.pack(expand=True, fill="both")
        txt = tk.Text(container, wrap="word", font=("Consolas", 11), padx=12, pady=10)
        yscroll = ttk.Scrollbar(container, command=txt.yview); txt.configure(yscrollcommand=yscroll.set)
        txt.pack(side="left", expand=True, fill="both"); yscroll.pack(side="right", fill="y")

        tutorial = f"""
Subnetting — the quick rules

1) A /prefix length splits 32 bits into Network | Host.
   host_bits = 32 - new_prefix
2) When you subnet a classful/private block further:
   subnet_bits = new_prefix - original_prefix
   number_of_subnets = 2^(subnet_bits)
3) Per-subnet host capacity (traditional):
   hosts_per_subnet = 2^(host_bits) - 2
   (except /31 and /32 → no traditional usable hosts)
4) Within the NEW subnet that contains the Host IP:
   network = first address (all host bits = 0)
   broadcast = last address (all host bits = 1)
   first_usable = network + 1
   last_usable  = broadcast - 1

Worked example (the one shown in your screenshot):

Given
  Host IP:                172.22.32.12
  Original subnet mask:   255.255.0.0     → original_prefix = /16
  New subnet mask:        255.255.224.0   → new_prefix      = /19

Step A — Bits
  subnet_bits = 19 - 16 = 3
  host_bits   = 32 - 19 = 13
  number_of_subnets = 2^3 = 8
  hosts_per_subnet  = 2^13 - 2 = 8190

Step B — Find the specific /19 containing 172.22.32.12
  /19 block size on the third octet is 32 (because 224 mask uses 3 bits):
   ranges: 0–31, 32–63, 64–95, ...
  32.12 falls into the 32–63 range → third octet = 32, host octet = 12

  Network address   = 172.22.32.0
  Broadcast address = 172.22.63.255
  First host        = 172.22.32.1
  Last host         = 172.22.63.254

That’s it. Now try the drills. If stuck, click “Show solution”.
(Pro tip: more compact notes & cheatsheets at terminalnotes.com)
"""
        txt.insert("1.0", textwrap.dedent(tutorial))
        txt.config(state="disabled")

    # -------- actions --------
    def new_problem(self):
        self.problem = Problem()
        self.lbl_hostip.set(dotted(self.problem.host_ip))
        self.lbl_origmask.set(dotted(self.problem.original_mask))
        self.lbl_newmask.set(dotted(self.problem.new_mask))
        self.lbl_newcidr.set(f"/{self.problem.new_prefix}")

        for w in self.inputs.values():
            if isinstance(w, ttk.Entry): w.delete(0, tk.END)
            else: w.set(None)
        self.status.configure(text="", foreground="green")

    def _ival(self, e):
        try: return int(e.get().strip())
        except: return None

    def _ipval(self, widget, allow_empty=False):
        try: v = widget.get()
        except ValueError: return "BAD"
        if v is None: return None
        try: return ipaddress.IPv4Address(v)
        except: return "BAD"

    def check_answers(self):
        sol = self.problem.solution()
        wrong = []

        if self._ival(self.inputs["Subnet bits"]) != int(sol["Subnet bits"]): wrong.append("Subnet bits")
        if self._ival(self.inputs["Number of subnets"]) != int(sol["Number of subnets"]): wrong.append("Number of subnets")
        if self._ival(self.inputs["Host bits per subnet"]) != int(sol["Host bits per subnet"]): wrong.append("Host bits per subnet")
        if self._ival(self.inputs["Hosts per subnet"]) != int(sol["Hosts per subnet"]): wrong.append("Hosts per subnet")

        if self._ipval(self.inputs["Network address"]) != ipaddress.IPv4Address(sol["Network address"]): wrong.append("Network address")

        first_expected = None if sol["First host"] == "" else ipaddress.IPv4Address(sol["First host"])
        fv = self._ipval(self.inputs["First host"], allow_empty=(first_expected is None))
        if first_expected is None:
            if fv is not None: wrong.append("First host")
        else:
            if fv != first_expected: wrong.append("First host")

        last_expected = None if sol["Last host"] == "" else ipaddress.IPv4Address(sol["Last host"])
        lv = self._ipval(self.inputs["Last host"], allow_empty=(last_expected is None))
        if last_expected is None:
            if lv is not None: wrong.append("Last host")
        else:
            if lv != last_expected: wrong.append("Last host")

        if self._ipval(self.inputs["Broadcast address"]) != ipaddress.IPv4Address(sol["Broadcast address"]): wrong.append("Broadcast address")

        if not wrong:
            self.status.configure(text="✅ Correct! New exercise loaded.", foreground="green")
            self.new_problem()
        else:
            self.status.configure(text=f"❌ Not quite. Check: {', '.join(wrong)}", foreground="red")

    def show_solution(self):
        sol = self.problem.solution()
        self.inputs["Subnet bits"].delete(0, tk.END); self.inputs["Subnet bits"].insert(0, sol["Subnet bits"])
        self.inputs["Number of subnets"].delete(0, tk.END); self.inputs["Number of subnets"].insert(0, sol["Number of subnets"])
        self.inputs["Host bits per subnet"].delete(0, tk.END); self.inputs["Host bits per subnet"].insert(0, sol["Host bits per subnet"])
        self.inputs["Hosts per subnet"].delete(0, tk.END); self.inputs["Hosts per subnet"].insert(0, sol["Hosts per subnet"])
        self.inputs["Network address"].set(sol["Network address"])
        self.inputs["First host"].set(sol["First host"] or None)
        self.inputs["Last host"].set(sol["Last host"] or None)
        self.inputs["Broadcast address"].set(sol["Broadcast address"])
        self.status.configure(text="👀 Solution filled in. Try a new exercise when ready.", foreground="blue")

# resource helper (works with PyInstaller)
def resource_path(rel):
    base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base, rel)

if __name__ == "__main__":
    App().mainloop()

Leave a Reply

Your email address will not be published. Required fields are marked *