telnet some-host 443 just to see if a port is open is a common reflex. That little reachability test — can this TCP port on this host be reached? — can be done natively in Python with the socket module, and from there it scales: sweep hundreds of ports across dozens of hosts in seconds. Today builds a real port reachability checker and, along the way, demystifies what a socket actually is.
No deep network-programming theory is needed here. TCP, ports, and the three-way handshake are already familiar territory for a network engineer — this is just about driving them from Python.
What a Socket Is (in 30 Seconds)
A socket is an endpoint for network communication — the programmatic equivalent of plugging a cable into a jack. Testing a TCP port means creating a socket, trying to connect it to (host, port), and seeing whether the connection succeeds. A successful connect means the port is open and something accepted the handshake. That is the whole idea.
import socket
# AF_INET = IPv4, SOCK_STREAM = TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2) # don't wait forever on a filtered port
result = s.connect_ex(("1.1.1.1", 443)) # returns 0 on success
s.close()
print("OPEN" if result == 0 else "CLOSED/FILTERED")
The key choice: connect_ex instead of connect. The plain connect raises an exception on failure; connect_ex returns an error code (0 = success), which allows clean branching without a try/except for the common case. For a scanner, connect_ex is exactly right.
Always Set a Timeout
This is the rule that separates a usable scanner from one that hangs. A closed port usually refuses fast, but a filtered port (firewall silently dropping) lets the connect attempt sit until the OS default timeout — which can be 30+ seconds. settimeout(2) caps that. Tune it to the network: 1–2 seconds for a LAN, more across a WAN.
The Cleaner Pattern: Context Managers
Recall with from the files lesson in Week 1. Sockets support it too, so they always get closed even if something goes wrong:
import socket
def check_port(host, port, timeout=2):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((host, port)) == 0
print(check_port("1.1.1.1", 443)) # True
print(check_port("1.1.1.1", 8443)) # False (likely)
That is a complete, reusable port checker in five lines. Everything else today builds on it.
Resolving Names and Grabbing the Local View
The socket module also wraps DNS and local identity, handy for inventory scripts:
import socket
# Forward lookup: name -> IP
print(socket.gethostbyname("one.one.one.one")) # 1.1.1.1
# Reverse lookup: IP -> name (may raise socket.herror if no PTR)
try:
print(socket.gethostbyaddr("1.1.1.1")[0])
except socket.herror:
print("no PTR record")
# What does the local box think it is?
print(socket.gethostname())
Cisco Context: A Service Health Checker
Put it together: scan the relevant management-plane ports across a device list and produce a matrix. Is SSH up on every switch? Did someone leave Telnet (23) open where they should not have? This answers both.
import socket
SERVICES = {22: "SSH", 23: "Telnet", 443: "HTTPS", 830: "NETCONF"}
def check_port(host, port, timeout=1.5):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((host, port)) == 0
devices = ["1.1.1.1", "8.8.8.8"]
# header
print(f"{'Host':16}" + "".join(f"{name:9}" for name in SERVICES.values()))
for host in devices:
row = f"{host:16}"
for port, name in SERVICES.items():
row += f"{'open' if check_port(host, port) else '-':9}"
print(row)
Run this and a row where Telnet shows open is an instant audit finding. The same structure, pointed at a real management subnet, becomes a daily exposure check.
Going Faster: A Note on Concurrency
Checking ports one at a time is fine for a handful of hosts but slow for hundreds, because each check spends most of its time waiting. That waiting is the perfect case for concurrency. Today’s examples stay single-threaded to keep the focus clear, but here is the shape of the speedup using the standard library’s thread pool — worth filing away for later:
from concurrent.futures import ThreadPoolExecutor
targets = [("1.1.1.1", 443), ("8.8.8.8", 443), ("1.1.1.1", 22)]
with ThreadPoolExecutor(max_workers=50) as pool:
results = pool.map(lambda t: (t, check_port(*t)), targets)
for (host, port), is_open in results:
print(host, port, "open" if is_open else "closed")
Exercises
- Warm-up. Write
is_open(host, port)usingconnect_exand a 2-second timeout. Test it against a known-open port and a known-closed one. - Range scan. Scan ports 20–25 on a host and print only the open ones.
- Service map. Given the dict
{22:"SSH", 80:"HTTP", 443:"HTTPS"}, check one host and print"SSH: open"/"HTTP: closed"style lines using the friendly names. - Resolve then scan. Take a hostname, resolve it to an IP with
gethostbyname, then check whether 443 is open on the resolved address. Handle the case where the name does not resolve. - Challenge. Write
first_open(host, ports)that returns the first open port from a list (useful for “is this device reachable on SSH or Telnet?”), orNoneif none are open. Then use it to classify a list of hosts as"ssh","telnet-only", or"unreachable".
Answers
Show answers
1. Warm-up
import socket
def is_open(host, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(2)
return s.connect_ex((host, port)) == 0
print(is_open("1.1.1.1", 443)) # True
print(is_open("1.1.1.1", 444)) # False
2. Range scan
host = "1.1.1.1"
for port in range(20, 26):
if is_open(host, port):
print(f"{port} open")
3. Service map
services = {22: "SSH", 80: "HTTP", 443: "HTTPS"}
host = "1.1.1.1"
for port, name in services.items():
print(f"{name}: {'open' if is_open(host, port) else 'closed'}")
4. Resolve then scan
import socket
name = "one.one.one.one"
try:
ip = socket.gethostbyname(name)
print(f"{name} -> {ip}: 443 {'open' if is_open(ip, 443) else 'closed'}")
except socket.gaierror:
print(f"{name} did not resolve")
gaierror is the “get address info” error — the exception DNS resolution raises on failure.
5. Challenge
def first_open(host, ports):
for p in ports:
if is_open(host, p):
return p
return None
hosts = ["1.1.1.1", "8.8.8.8", "192.0.2.1"]
for h in hosts:
p = first_open(h, [22, 23])
if p == 22:
label = "ssh"
elif p == 23:
label = "telnet-only"
else:
label = "unreachable"
print(f"{h:15} {label}")
Returning early from the loop on the first hit is the idiom — no need to scan the rest once an answer is in hand.
This is Day 11 of the 21‑post Python for Network Engineers series.
Previously: subprocess. Coming tomorrow — Paramiko: a first real SSH session to a router, driven entirely from Python.