Sharing notes from my ongoing learning journey — what I build, break and understand along the way.
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)
- Open the app — the Given block at the top displays the Host IP, Original mask and New mask.
- Below, enter your answers: numeric fields and four-octet IP boxes. Octet fields auto-advance as you type.
- 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.
- 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()