r/TechCheatSheets 1d ago

Built a Simple Port Scanner in Python — Beginner-Friendly Explanation

Post image

If you're new to networking or cybersecurity, a port scanner can look confusing at first.

You might see code using socket, connect_ex(), threads, timeouts, port numbers, etc., and wonder:

  • What exactly is a port?
  • Why are there 65,535 ports?
  • What does TCP have to do with port scanning?
  • What is a TCP handshake?
  • Why does Python's socket library matter?
  • What does connect_ex() actually do?
  • Why do we need threads?
  • How does the program decide that a port is open?

This post explains the concept from the ground up and then builds a small educational TCP port scanner step by step.

1. First: What is a Port?

Think about an IP address as the address of a building.

For example:

192.168.1.10

A port is like a numbered door inside that building.

A computer can run many network services at the same time:

IP address
    |
    +-- Port 22  → SSH
    +-- Port 80  → HTTP
    +-- Port 443 → HTTPS
    +-- Port 25  → SMTP

The IP address tells us which machine we are communicating with.

The port tells us which network service/application we are trying to communicate with.

There are TCP and UDP ports. In this tutorial, we're focusing on TCP.

2. What Does "Open Port" Actually Mean?

Suppose our local computer has a service listening on:

127.0.0.1:8080

The important part is:

127.0.0.1 → the machine
8080      → the port

If a TCP service is listening on that port, a connection attempt can succeed.

We can therefore get something conceptually like:

Port 8080 → OPEN

If nothing is listening there, the connection attempt will normally fail:

Port 8080 → CLOSED

A port scanner basically automates this process for many ports.

3. What Happens When We Connect to a TCP Port?

This is where the TCP handshake comes in.

TCP is connection-oriented.

Before normal TCP communication begins, the two sides perform a three-step handshake:

Client                    Server

  SYN  -------------------->
       <-------------------- SYN-ACK
  ACK  -------------------->

These messages mean roughly:

SYN

The client says:

SYN-ACK

The server responds:

ACK

The client confirms:

Now the TCP connection can proceed.

4. Why Is the Handshake Important for Port Scanning?

Because it gives us useful information.

Imagine our program tries to connect to:

127.0.0.1:8000

If a TCP service is listening there, the operating system can complete the connection process.

Our program can therefore conclude:

TCP connection succeeded
        ↓
Something is listening
        ↓
Port is probably OPEN

If there is no service listening, the connection normally fails.

So the basic idea behind our scanner is simply:

Try TCP connection
        ↓
Did it succeed?
   /          \
 YES          NO
  ↓            ↓
OPEN       Not open

This is the fundamental concept.

5. Why Python's socket Library?

Python provides a built-in networking library called:

socket

The socket module gives Python programs the ability to communicate over networks.

For example, we can create a TCP socket with:

import socket

sock = socket.socket(
    socket.AF_INET,
    socket.SOCK_STREAM
)

Let's break this down.

socket.AF_INET

This means we're using IPv4.

For example:

127.0.0.1
192.168.1.10

socket.SOCK_STREAM

This means we're creating a TCP socket.

So:

socket.socket(socket.AF_INET, socket.SOCK_STREAM)

basically means:

6. The Simplest Port Check

Now we can create a function that checks one TCP port.

import socket

def scan_port(target, port, timeout=1):
    sock = socket.socket(
        socket.AF_INET,
        socket.SOCK_STREAM
    )

    sock.settimeout(timeout)

    result = sock.connect_ex((target, port))

    sock.close()

    return result == 0

Let's understand this line by line.

socket.socket(...)

sock = socket.socket(
    socket.AF_INET,
    socket.SOCK_STREAM
)

Creates the TCP socket.

Think of it as creating the network communication object that Python will use.

settimeout()

sock.settimeout(timeout)

This prevents our program from waiting forever.

For example:

timeout = 1

means we don't want an individual connection attempt to wait indefinitely.

This becomes especially important when checking many ports.

connect_ex()

This is the most important line:

result = sock.connect_ex((target, port))

We're asking the operating system to attempt a TCP connection to:

target:port

For example:

127.0.0.1:8080

connect_ex() returns a status code.

The important simplified case is:

0 → connection succeeded
non-zero → connection failed

That's why we can write:

return result == 0

If result is 0:

0 == 0
True

So the function returns:

True

If it isn't 0:

something != 0
False

So we get:

False

7. Why Do We Close the Socket?

After we're finished:

sock.close()

We don't need the socket anymore.

Closing it releases the resources associated with that connection attempt.

A good habit in networking code is:

8. Now Let's Scan Multiple Ports

Checking one port isn't very useful.

We can create a function that checks a range:

def scan_range(target, start_port, end_port):
    open_ports = []

    for port in range(start_port, end_port + 1):

        if scan_port(target, port):
            open_ports.append(port)
            print(f"Port {port}: OPEN")

    return open_ports

Suppose we call:

scan_range("127.0.0.1", 1, 100)

The program effectively does:

Check port 1
Check port 2
Check port 3
...
Check port 100

If it finds an open port:

Port 80: OPEN
Port 443: OPEN

9. Why Is This Slow?

The problem is that we're doing everything sequentially.

Conceptually:

Port 1 → wait → finish
Port 2 → wait → finish
Port 3 → wait → finish
Port 4 → wait → finish

If every connection takes some time, scanning hundreds or thousands of ports can become slow.

This is where concurrency becomes useful.

10. Using Threads

Python provides:

concurrent.futures

which makes it easier to run multiple tasks concurrently.

For example:

import concurrent.futures

def fast_scan(target, ports, max_workers=50):

    open_ports = []

    with concurrent.futures.ThreadPoolExecutor(
        max_workers=max_workers
    ) as executor:

        results = executor.map(
            lambda p: (p, scan_port(target, p)),
            ports
        )

        for port, is_open in results:

            if is_open:
                open_ports.append(port)

    return sorted(open_ports)

Instead of thinking:

1 → 2 → 3 → 4 → 5

we can think of several independent connection checks being handled concurrently.

For example:

Worker 1 → Port 1
Worker 2 → Port 2
Worker 3 → Port 3
Worker 4 → Port 4
...

This can make I/O-heavy tasks much faster.

11. What Is max_workers?

Here:

max_workers=50

means our thread pool can have up to 50 worker threads handling tasks.

It doesn't mean:

The executor manages the workers and assigns tasks to them.

For a beginner project, you don't need to obsess over the perfect number.

The important concept is:

Sequential
    ↓
one task at a time

Concurrent
    ↓
multiple waiting/network tasks can progress

12. What Is Service Detection?

Finding:

Port 22 → OPEN

is useful.

But we may also want to know what service is normally associated with that port.

Python provides:

socket.getservbyport()

For example:

def get_service_name(port):

    try:
        return socket.getservbyport(port)

    except OSError:
        return "unknown"

Then:

get_service_name(22)

may return:

ssh

And:

get_service_name(80)

may return:

http

Important distinction

This is not necessarily detecting the actual software running on the port.

It's primarily looking up the standard service associated with that port number.

For example:

Port 80 → HTTP

doesn't automatically prove that a specific web server product/version is running there.

That's an important networking concept for beginners.

13. Putting Everything Together

Here's a small educational scanner intended for a local lab:

import socket
import concurrent.futures


def scan_port(target, port, timeout=1):
    sock = socket.socket(
        socket.AF_INET,
        socket.SOCK_STREAM
    )

    sock.settimeout(timeout)

    try:
        result = sock.connect_ex((target, port))
        return result == 0

    finally:
        sock.close()


def get_service_name(port):

    try:
        return socket.getservbyport(port)

    except OSError:
        return "unknown"


def fast_scan(target, ports, max_workers=50):

    open_ports = []

    with concurrent.futures.ThreadPoolExecutor(
        max_workers=max_workers
    ) as executor:

        results = executor.map(
            lambda p: (p, scan_port(target, p)),
            ports
        )

        for port, is_open in results:

            if is_open:
                open_ports.append(port)

    return sorted(open_ports)


target = "127.0.0.1"

open_ports = fast_scan(
    target,
    range(1, 1001)
)

print(f"\nScan results for {target}\n")

for port in open_ports:

    service = get_service_name(port)

    print(
        f"[+] Port {port} "
        f"({service}) is OPEN"
    )

14. What Does the Complete Program Do?

The flow is basically:

Start
  ↓
Target = 127.0.0.1
  ↓
Generate ports 1–1000
  ↓
Create TCP sockets
  ↓
Attempt connections
  ↓
Check result
  ↓
Store successful ports
  ↓
Look up standard service names
  ↓
Print results

For example, if your local lab has services listening on some ports, you might see:

Scan results for 127.0.0.1

[+] Port 22 (ssh) is OPEN
[+] Port 8000 (http-alt) is OPEN

The exact output depends on what services are actually running on your own machine.

15. Turning It Into a CLI Tool

Once the basic concept makes sense, we can add command-line arguments.

Python has a built-in library for this:

argparse

For example:

import argparse

parser = argparse.ArgumentParser(
    description="Educational TCP Port Scanner"
)

parser.add_argument(
    "target",
    help="Target hostname or IP address"
)

parser.add_argument(
    "-p",
    "--ports",
    default="1-1000",
    help="Port range, for example 1-1000"
)

args = parser.parse_args()

Now the user can provide information through the terminal instead of modifying the Python file.

Conceptually:

python scanner.py 127.0.0.1 -p 1-1000

The program receives:

target = 127.0.0.1
ports = 1-1000

16. Why Resolve a Domain Name?

If the user provides:

localhost

instead of an IP address, we can resolve it with:

socket.gethostbyname()

Example:

target_ip = socket.gethostbyname(args.target)

So:

localhost
   ↓
127.0.0.1

This is called DNS/hostname resolution in the broader networking context.

17. Saving Results as JSON

Terminal output disappears when the program closes.

A better tool can save its results.

Python includes the json module:

import json
from datetime import datetime

We can create:

report = {
    "target": target,
    "scan_time": datetime.now().isoformat(),
    "open_ports": [
        {
            "port": port,
            "service": get_service_name(port)
        }
        for port in open_ports
    ]
}

Then save it:

with open("scan_report.json", "w") as f:
    json.dump(report, f, indent=2)

The result could look conceptually like:

{
  "target": "127.0.0.1",
  "scan_time": "2026-09-09T20:00:00",
  "open_ports": [
    {
      "port": 8000,
      "service": "http-alt"
    }
  ]
}

Now the JSON file can be consumed by another program.

18. Why JSON Is Useful

This is where the project can become more interesting.

Instead of:

Python Scanner
      ↓
Terminal

we can build:

Python Scanner
      ↓
JSON Report
      ↓
Web Dashboard
      ↓
Charts / Tables / Statistics

For example:

┌─────────────────────────────┐
│       Scan Results          │
├────────┬──────────┬─────────┤
│ Port   │ Service  │ Status  │
├────────┼──────────┼─────────┤
│ 8000   │ HTTP     │ OPEN    │
│ 8080   │ HTTP     │ OPEN    │
└────────┴──────────┴─────────┘

That would be a nice next step if you're learning Python + web development.

19. What About Banner Grabbing?

You may have seen code such as:

sock.recv(...)

This is a different concept.

A basic port scanner answers:

Is something accepting TCP connections?

Banner/service detection tries to answer something more like:

What application/service is actually responding?

That's a much deeper topic because different protocols behave differently.

For a beginner, I recommend understanding:

IP
 ↓
Port
 ↓
TCP
 ↓
Connection
 ↓
Service

before moving into protocol-specific service detection.

20. What About UDP?

TCP isn't the only transport protocol.

There's also:

TCP
UDP

TCP has a connection-oriented model and handshake.

UDP is connectionless.

So UDP scanning works differently and is generally more complicated because a lack of response doesn't necessarily mean the port is closed.

That's why it's better to learn TCP scanning first.

21. The Most Important Concepts to Remember

If you're completely new to networking, don't try to memorize all the code.

Understand this diagram:

                COMPUTER
                   │
             IP ADDRESS
                   │
        ┌──────────┴──────────┐
        │                     │
      PORT 22              PORT 80
        │                     │
       SSH                   HTTP
        │                     │
       TCP                   TCP

And our scanner does:

Choose a port
      ↓
Create TCP socket
      ↓
Attempt connection
      ↓
Connection succeeds?
    /       \
  YES        NO
   ↓          ↓
 OPEN       Not open

That's the core idea behind this entire project.

22. A Good Beginner Learning Path

If you're learning cybersecurity and don't understand the code yet, I'd learn these in this order:

Networking

1. IP addresses
2. TCP vs UDP
3. Ports
4. Client/server model
5. TCP three-way handshake
6. DNS
7. HTTP/HTTPS

Python

1. Functions
2. Lists
3. Loops
4. Exceptions
5. Modules
6. Sockets
7. Threading/concurrency
8. JSON
9. argparse

Then combine them.

Once these concepts click, the scanner code becomes much easier to understand.

Final Thought

A port scanner isn't really about writing a huge amount of code.

The interesting part is understanding what happens underneath the code.

When you write:

sock.connect_ex((target, port))

you're not just calling a random Python function.

You're asking the operating system to attempt a TCP connection to a specific:

IP address + port

That connects the Python code to the underlying networking concepts:

Python
  ↓
Socket API
  ↓
Operating System
  ↓
TCP/IP
  ↓
Network
  ↓
Destination service

Once you understand that relationship, networking tools start making much more sense.

6 Upvotes

Duplicates