A function returns one value. But a scan result is more than one value: it is a host, a port, whether the port was open, and when you checked. You could juggle four separate variables for every result, or you could bundle them into one tidy thing that carries its own information around. That bundle is called an object, and the pattern that makes it is called a class.
Analogy: The rubber stamp
A class is a rubber stamp; an object is one imprint it makes. The stamp is cut once and describes the shape of every imprint. Each imprint is a separate mark on the page, and two imprints never smudge into one another.
Where a function returns a single value, an object built from a class can hold several related pieces of information at once and offer methods that read or change that information. A scan result is a natural fit: it needs to remember a host, a port, whether the port was open, and when the check was made.
Your first class
scan_result.py
import datetime
class ScanResult:
def __init__(self, host, port):
self.host = host
self.port = port
self.is_open = False
self.checked_at = None
def mark_open(self):
self.is_open = True
self.checked_at = datetime.datetime.now()
def __str__(self):
state = "open" if self.is_open else "closed"
return f"{self.host}:{self.port} is {state}"
result = ScanResult("10.0.0.5", 22)
result.mark_open()
print(result) # 10.0.0.5:22 is open
Sample output
10.0.0.5:22 is open
The anatomy, part by part
| Part | What it is | Why it is there |
|---|---|---|
class ScanResult: | The stamp: one definition describing every imprint | Written once, used as often as you like |
__init__(self, host, port) | The constructor. It runs automatically the moment a new object is created | Sets up the object's starting state: this host, this port, not yet open, not yet checked |
self | The particular imprint a method is acting on | It is what lets two ScanResult objects track two different hosts without interfering |
self.is_open = False | An attribute: a piece of information the object remembers | State that lives as long as the object does |
mark_open(self) | A method: a function that belongs to the object | It reads or changes the object's own state |
__str__(self) | Decides what print() shows for the object | Worth defining on any class whose instances you plan to log |
Python concept: Why every method starts with self
When you write result.mark_open(), Python quietly passes the object result as the first argument, and the method receives it under the name self. Inside the method, self.is_open = True means "set is_open on this imprint". That single idea is what keeps imprints from smudging into one another.
Two imprints, never smudged
Create a second result and change only the first. The second does not notice:
two_objects.py
result = ScanResult("10.0.0.5", 22)
result.mark_open()
another = ScanResult("10.0.0.7", 3306)
print(result)
print(another)
print(result.is_open, another.is_open)
print(result.checked_at is not None, another.checked_at)
print(type(result).__name__)
Sample output
10.0.0.5:22 is open
10.0.0.7:3306 is closed
True False
True None
ScanResult
result was marked open and has a timestamp. another was never touched, so it is still closed and its checked_at is still None. Each object owns its own state.
Going further: an object that holds objects
Objects can hold other objects. A scan of one host produces many results, so a report can keep a list of ScanResult objects and answer questions about them. This is the same pattern a real scanner uses, in miniature.
scan_report.py
class ScanReport:
def __init__(self, host):
self.host = host
self.results = [] # a list of ScanResult objects
def add(self, port, is_open):
r = ScanResult(self.host, port)
if is_open:
r.mark_open()
self.results.append(r)
def open_ports(self):
return [r.port for r in self.results if r.is_open]
def summary(self):
return f"{self.host}: {len(self.open_ports())} of {len(self.results)} ports open {self.open_ports()}"
report = ScanReport("10.0.0.5")
for port, state in [(22, True), (80, True), (443, False), (3306, False)]:
report.add(port, state)
print(report.summary())
for r in report.results:
print(" ", r)
Sample output
10.0.0.5: 2 of 4 ports open [22, 80]
10.0.0.5:22 is open
10.0.0.5:80 is open
10.0.0.5:443 is closed
10.0.0.5:3306 is closed
The line [r.port for r in self.results if r.is_open] is a list comprehension: a loop, a condition and a list, written on one line. Read it aloud: "the port of every result, in the results, that is open". You will meet it again in the file-handling article.
Case study: Scenario: one object per host in a ransomware sweep
After a ransomware scare, an analyst needs to know which of the college's twelve servers expose the Windows file-sharing port. Twelve separate variables for each host, plus a list of which ports were open, plus timestamps, would turn the script into a mess. Twelve ScanReport objects, each created from the same class, each answering summary() for its own host, keep the script short and the output readable. This scenario is illustrative, but it is exactly how the port scanner you will build in Unit 2 organises its data.
Check your understanding
Question 1: In your own words, what is the difference between a class and an object?
A class is a single definition that describes the shape of something, like a rubber stamp. An object is one particular instance made from that class, like one imprint. You write the class once and can create as many objects from it as you need, each with its own independent state.
Question 2: What does init do, and when does it run?
__init__ is the constructor. It runs automatically at the moment a new object is created, for example when you write ScanResult("10.0.0.5", 22), and it sets up the object's starting state.
Question 3: Why define str on a class you plan to log?
__str__ decides what print() (and string formatting) shows for an object. Without it you would see an unhelpful default like an object's memory address. With it, a log line reads 10.0.0.5:22 is open.
Key takeaway: Key takeaway
A class is a stamp; an object is one imprint. __init__ sets up the starting state, self keeps imprints apart, methods change state, and __str__ makes an object printable. Scan results, parsed log lines and packets all fit this shape.

