A script that cannot decide, repeat or reuse anything is just a calculator. This article gives you the three ideas that turn a few lines into a tool: conditions (decide), loops (repeat) and functions (reuse). By the end you will have written the first working piece of a port scanner.
Conditions: choosing a path
Python decides which lines belong to an if by their indentation, not by braces. A missing or uneven indent is the single most common error a beginner meets, and is worth checking first whenever a script refuses to run.
conditions.py
port = 443
if port == 443:
print("Likely HTTPS")
elif port == 80:
print("Likely HTTP")
else:
print("Unrecognised common port")
Sample output
Likely HTTPS
Python checks the conditions from the top and runs the first block whose condition is true, then skips the rest. The else at the bottom catches everything nothing above it claimed.
Caution: The error you will meet on day one
Forget to indent the line under if, and Python refuses to run the script at all:
File "conditions.py", line 3
print("Likely HTTPS")
^^^^^
IndentationError: expected an indented block after 'if' statement on line 2
Read the line number, look at the line above it, and check the indentation. Four spaces per level is the convention. Never mix tabs and spaces in the same file.
Loops: the night watchman's rounds
Analogy: The night watchman's rounds
A for loop is a watchman walking a fixed set of doors, one after another, until every door on the list has been checked. A while loop is a watchman told to keep walking until a condition changes, useful when you do not know in advance how many rounds are needed, such as reading from a socket until the other side stops sending.
loops.py
# for: a known, fixed set of doors to check
for port in [21, 22, 23, 25, 80, 443]:
print(f"Would check port {port}")
# while: keep reading from a socket until no more data arrives
data = sock.recv(1024)
while data:
process(data)
data = sock.recv(1024)
The second half of that listing is a pattern, not a program to run yet: sock is a network socket you will create in the sockets article, and process stands for whatever your script does with each chunk of data. The shape is what matters. It reads once to start, then keeps going while the last read returned something, and stops the moment the other side has nothing more to send.
range(start, stop) produces the integers from start up to, but not including, stop, and is the usual way to sweep a block of port numbers:
range_demo.py
for port in range(1, 1025): # ports 1 through 1024
pass # a scanner checks each one here
print(list(range(1, 6))) # [1, 2, 3, 4, 5]
print(list(range(10, 0, -3))) # a third number is the step: [10, 7, 4, 1]
Functions: the recipe card
Analogy: The recipe card
A function is a recipe card. Once written, it can be followed again whenever the dish is needed, without copying out every step each time. Keeping each function focused on one task, one dish, also makes a script easier to test and to reuse in a later unit.
is_port_open.py
def is_port_open(host, port, timeout=1.0):
"""Return True if a TCP connection to host:port succeeds."""
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
sock.close()
return result == 0
if is_port_open("10.0.0.5", 22):
print("SSH port appears open")
| Line | What it does |
|---|---|
def is_port_open(host, port, timeout=1.0): | Starts the recipe card. Three inputs; timeout has a default of 1.0, so it is optional |
"""Return True if ...""" | A docstring: one sentence saying what the function does, for the next person to read |
import socket | Brings in Python's networking module (you will meet it fully in the sockets article) |
socket.socket(AF_INET, SOCK_STREAM) | Creates a TCP socket, a handle on one end of a connection |
sock.settimeout(timeout) | Gives up waiting after this many seconds instead of hanging forever |
sock.connect_ex((host, port)) | Tries to connect and returns a number instead of raising an error. 0 means success |
sock.close() | Hangs up, whatever the outcome |
return result == 0 | Answers True when the connection succeeded, False otherwise |
The timeout=1.0 parameter carries a default value, so the function can be called as is_port_open("10.0.0.5", 22) without a timeout, or as is_port_open("10.0.0.5", 22, timeout=3.0) when a slow network needs more patience. You will meet this exact function again, grown into a full scanner, at the start of Unit 2.
Try it on your own machine
You do not need a lab server to watch this function work. Python can open a real port for you with a one-line web server. In one terminal, start it:
terminal 1
$ python3 -m http.server 9999 --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 9999 (http://127.0.0.1:9999/) ...
In a second terminal, sweep a block of ports on your own machine using the for loop, range and your function together:
sweep_demo.py
import socket
def is_port_open(host, port, timeout=1.0):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
sock.close()
return result == 0
for port in range(9996, 10001):
state = "open" if is_port_open("127.0.0.1", port) else "closed"
print(f"127.0.0.1:{port:<5} {state}")
Sample output
127.0.0.1:9996 closed
127.0.0.1:9997 closed
127.0.0.1:9998 closed
127.0.0.1:9999 open
127.0.0.1:10000 closed
You have just written the heart of a port scanner: a loop that visits each door, a function that knocks, and a condition that reports. Everything else a scanner does is polish on those three ideas.
Combining all three: wait for a service, then sweep
Real scripts mix while, for, functions and conditions freely. This one waits patiently for a service to come up, then lists which ports in a block are open. Start it first, then launch the web server from the previous step a couple of seconds later and watch the loop notice.
wait_and_sweep.py
import socket
import time
def is_port_open(host, port, timeout=1.0):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
sock.close()
return result == 0
def wait_for_port(host, port, max_tries=6, pause=1.0):
attempt = 1
while attempt <= max_tries: # while: we do not know how many rounds
if is_port_open(host, port):
print(f"Attempt {attempt}: {host}:{port} is open")
return True
print(f"Attempt {attempt}: {host}:{port} not up yet")
time.sleep(pause)
attempt += 1
return False
def sweep(host, ports):
open_ports = []
for port in ports: # for: a known list of doors
if is_port_open(host, port, timeout=0.5):
open_ports.append(port)
return open_ports
print(wait_for_port("127.0.0.1", 9999))
print("Open ports:", sweep("127.0.0.1", range(9990, 10000)))
Sample output
Attempt 1: 127.0.0.1:9999 not up yet
Attempt 2: 127.0.0.1:9999 not up yet
Attempt 3: 127.0.0.1:9999 is open
True
Open ports: [9999]
Real incident: Mirai, 2016: a loop and a short list
In the autumn of 2016, a botnet named Mirai infected hundreds of thousands of internet-connected cameras, routers and video recorders by trying a short, fixed list of around sixty factory-default username and password pairs, admin and admin among them, over Telnet. No exploit was needed; the devices had simply never had their defaults changed. On 21 October 2016, Mirai directed its captured devices to flood Dyn, a company providing DNS for a large share of the internet, taking Twitter, Netflix, Reddit and many other sites offline for hours across the United States.
What this teaches: the idea behind Mirai is the idea in this article. It is a for loop over a short list, repeated across an enormous number of doors. Mirai reached a scale no single attacker could by hand only because so many devices shared one unchanged default. You will build the harmless, lab-only version of that loop in Unit 2, and by then you will understand exactly why changing a default credential before a device goes into service remains one of the cheapest and most effective defences in all of security.
Check your understanding
Question 1: When would you choose a while loop over a for loop?
Choose for when you have a known, fixed set of items to visit, such as a list of ports. Choose while when you do not know in advance how many rounds you need, such as reading from a socket until the other side stops sending, or retrying until a service comes up.
Question 2: What does range(1, 1025) produce, and what is the last port it includes?
It produces the integers 1 through 1024. The stop value is not included, so 1025 never appears. The last port is 1024.
Question 3: In is_port_open(host, port, timeout=1.0), what does the "= 1.0" mean, and how do you override it?
It gives timeout a default value. If the caller leaves it out, 1.0 seconds is used. To override it, pass a value: is_port_open("10.0.0.5", 22, timeout=3.0).
Key takeaway: Key takeaway
Conditions decide, loops repeat and functions reuse. Indentation is syntax, for suits a known list and while suits an unknown number of rounds, and a small function like is_port_open() is the seed of a whole scanner.

