Simple Port Scanning with python-nmap

Simple Port scanning with python-nmap

For anyone learning network security or building their first automation tool, port scanning is usually the very first practical skill to pick up. While Nmap’s command line is powerful on its own, wrapping it in Python using the python-nmap library opens the door to building custom tools — asset inventories, monitoring scripts, or simple vulnerability triage tools — without reinventing the wheel of scan-result parsing.

This guide focuses specifically on the fundamentals: getting a basic port scan running in Python, understanding the result structure, and building small but genuinely useful scripts on top of it.

Prerequisites

Before starting, make sure both Nmap and python-nmap are installed:

# Install Nmap (Debian/Ubuntu example)
sudo apt install nmap

# Install python-nmap
pip install python-nmap

Verify both are working:

nmap --version
python3 -c "import nmap; print(nmap.__version__)"

Your First Port Scan Script

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '22-443')

for host in scanner.all_hosts():
    print(f"Host: {host}")
    print(f"State: {scanner[host].state()}")

Note: scanme.nmap.org is a host explicitly maintained by the Nmap project for testing and learning purposes, making it safe to scan without needing separate authorization.

Expected output:

Host: 45.33.32.156
State: up

Scanning Specific Ports

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '22,80,443')

for host in scanner.all_hosts():
    for proto in scanner[host].all_protocols():
        ports = scanner[host][proto].keys()
        for port in sorted(ports):
            state = scanner[host][proto][port]['state']
            print(f"Port {port}: {state}")

Expected output:

Port 22: open
Port 80: open
Port 443: open

Scanning a Port Range

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '1-1000')

open_ports = []
for host in scanner.all_hosts():
    for proto in scanner[host].all_protocols():
        for port, data in scanner[host][proto].items():
            if data['state'] == 'open':
                open_ports.append(port)

print(f"Open ports: {sorted(open_ports)}")

Expected output:

Open ports: [22, 80]

Scanning the Top Common Ports

If a specific range isn’t needed, you can let Nmap pick its default “most common” ports using the -F (fast scan) argument, which checks the 100 most commonly used ports rather than a full range.

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', arguments='-F')

for host in scanner.all_hosts():
    print(f"Host: {host}, State: {scanner[host].state()}")
    for proto in scanner[host].all_protocols():
        for port, data in scanner[host][proto].items():
            print(f"  Port {port}: {data['state']}")

Getting Service Names Along with Port States

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '1-1000', arguments='-sV')

for host in scanner.all_hosts():
    for proto in scanner[host].all_protocols():
        for port, data in scanner[host][proto].items():
            print(f"Port {port}/{proto}: {data['state']} - {data.get('name', 'unknown')} {data.get('version', '')}")

Expected output:

Port 22/tcp: open - ssh OpenSSH 6.6.1p1
Port 80/tcp: open - http Apache httpd 2.4.7

The -sV argument triggers version detection, populating additional fields like name, product, and version within each port’s data dictionary.

Building a Simple Reusable Port Scanner Function

import nmap

def simple_port_scan(target, ports='1-1024'):
    scanner = nmap.PortScanner()
    scanner.scan(target, ports)

    results = []
    for host in scanner.all_hosts():
        for proto in scanner[host].all_protocols():
            for port, data in scanner[host][proto].items():
                results.append({
                    'host': host,
                    'port': port,
                    'protocol': proto,
                    'state': data['state'],
                    'service': data.get('name', 'unknown')
                })
    return results

if __name__ == '__main__':
    results = simple_port_scan('scanme.nmap.org', '1-100')
    for entry in results:
        print(entry)

Expected output:

{'host': '45.33.32.156', 'port': 22, 'protocol': 'tcp', 'state': 'open', 'service': 'ssh'}
{'host': '45.33.32.156', 'port': 80, 'protocol': 'tcp', 'state': 'open', 'service': 'http'}

This kind of function is a good building block for larger tools — it can be imported into other scripts, wrapped in a CLI interface, or connected to a scheduler for periodic scans.

Scanning Multiple Hosts in a Simple Loop

import nmap

scanner = nmap.PortScanner()
targets = ['scanme.nmap.org', '127.0.0.1']

for target in targets:
    scanner.scan(target, '1-1024')
    for host in scanner.all_hosts():
        print(f"\nResults for {host}:")
        for proto in scanner[host].all_protocols():
            for port, data in scanner[host][proto].items():
                if data['state'] == 'open':
                    print(f"  Port {port}/{proto} open - {data.get('name', 'unknown')}")

Filtering Only Open Ports

Since scan results include closed and filtered ports too (depending on scan type), it’s often useful to filter down to just what matters:

import nmap

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '1-1000')

for host in scanner.all_hosts():
    open_ports = [
        port for proto in scanner[host].all_protocols()
        for port, data in scanner[host][proto].items()
        if data['state'] == 'open'
    ]
    print(f"{host} has {len(open_ports)} open ports: {sorted(open_ports)}")

Exporting Results to a File

import nmap
import json

scanner = nmap.PortScanner()
scanner.scan('scanme.nmap.org', '1-1000')

results = {}
for host in scanner.all_hosts():
    results[host] = {}
    for proto in scanner[host].all_protocols():
        results[host][proto] = scanner[host][proto]

with open('scan_results.json', 'w') as f:
    json.dump(results, f, indent=2)

print("Results saved to scan_results.json")

This makes it easy to feed scan data into other tools, dashboards, or simple reporting scripts without re-running the scan every time.

Understanding the Result Dictionary Structure

It helps to know exactly what python-nmap returns under the hood. A simplified view of the structure for one host looks like this:

{
    '192.168.1.10': {
        'tcp': {
            22: {'state': 'open', 'name': 'ssh', 'product': 'OpenSSH', 'version': '8.2p1'},
            80: {'state': 'open', 'name': 'http', 'product': 'Apache httpd', 'version': '2.4.41'}
        }
    }
}

Every host is keyed by IP address, then by protocol (tcp or udp), then by port number, with a dictionary of details as the value.

Troubleshooting Common Issues

Empty all_hosts() result Cause: The host might be down, blocking ICMP, or the scan target is unreachable. Fix: Try adding -Pn to the arguments to skip host discovery and force a port scan regardless of ping response:

scanner.scan('scanme.nmap.org', '1-1000', arguments='-Pn')

“You need to be root to run this scan type” Cause: SYN scan (-sS) or other privileged scan types were attempted without sufficient permissions. Fix: Run the script with sudo (Linux/macOS) or switch to a TCP connect scan (default for unprivileged users).

Very slow scans Cause: Scanning a large port range without specifying faster timing. Fix: Add a timing template argument, e.g., arguments='-T4', to speed up the scan (be mindful this increases network noise).

KeyError on scanner[host][proto] Cause: The specified protocol wasn’t actually scanned (e.g., checking udp when only a TCP scan was run). Fix: Always check scanner[host].all_protocols() before accessing a specific protocol key.

Security Best Practices

Limitations

Adding Basic Command-Line Arguments to Your Scanner

To make the simple scanner script from earlier more reusable, command-line arguments can be added using the built-in argparse module rather than hardcoding targets and port ranges:

import nmap
import argparse

def simple_port_scan(target, ports='1-1024'):
    scanner = nmap.PortScanner()
    scanner.scan(target, ports)

    results = []
    for host in scanner.all_hosts():
        for proto in scanner[host].all_protocols():
            for port, data in scanner[host][proto].items():
                results.append({
                    'host': host,
                    'port': port,
                    'protocol': proto,
                    'state': data['state'],
                    'service': data.get('name', 'unknown')
                })
    return results

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Simple port scanner using python-nmap')
    parser.add_argument('target', help='Target host or IP address')
    parser.add_argument('--ports', default='1-1024', help='Port range to scan (default: 1-1024)')
    args = parser.parse_args()

    results = simple_port_scan(args.target, args.ports)
    for entry in results:
        if entry['state'] == 'open':
            print(f"{entry['host']}:{entry['port']}/{entry['protocol']} - {entry['service']}")

Usage:

python3 scanner.py scanme.nmap.org --ports 1-1000

This small addition turns a fixed script into a genuinely reusable command-line tool that can be shared with teammates or integrated into other automation pipelines.

Comparing Results Between Two Scans

A common practical use case is checking whether a host’s open ports have changed since a previous scan — useful for spotting newly exposed services or confirming that a remediation closed a port as expected.

import nmap

def get_open_ports(target, ports='1-1024'):
    scanner = nmap.PortScanner()
    scanner.scan(target, ports)
    open_ports = set()
    for host in scanner.all_hosts():
        for proto in scanner[host].all_protocols():
            for port, data in scanner[host][proto].items():
                if data['state'] == 'open':
                    open_ports.add(port)
    return open_ports

before = {22, 80, 443}  # Example: previously recorded open ports
after = get_open_ports('scanme.nmap.org', '1-1000')

new_ports = after - before
closed_ports = before - after

print(f"Newly opened ports: {sorted(new_ports)}")
print(f"Newly closed ports: {sorted(closed_ports)}")

This kind of comparison logic is the basis for simple drift-detection scripts that flag configuration changes over time without needing a full monitoring platform.

Building a Minimal Open-Port Report

For a small but genuinely useful deliverable, here’s a script that scans a target and produces a clean, readable summary — the kind of thing you might attach to an internal ticket or share with a teammate without needing them to parse raw Nmap output themselves.

import nmap
from datetime import datetime

def generate_report(target, ports='1-1024'):
    scanner = nmap.PortScanner()
    scanner.scan(target, ports, arguments='-sV')

    lines = []
    lines.append(f"Port Scan Report")
    lines.append(f"Target: {target}")
    lines.append(f"Generated: {datetime.now().isoformat()}")
    lines.append("-" * 40)

    for host in scanner.all_hosts():
        lines.append(f"Host: {host} ({scanner[host].hostname() or 'no hostname'})")
        lines.append(f"State: {scanner[host].state()}")

        for proto in scanner[host].all_protocols():
            open_count = 0
            for port, data in sorted(scanner[host][proto].items()):
                if data['state'] == 'open':
                    open_count += 1
                    service = data.get('name', 'unknown')
                    version = data.get('version', '')
                    lines.append(f"  {port}/{proto} open - {service} {version}".rstrip())
            lines.append(f"Total open {proto.upper()} ports: {open_count}")

    return "\n".join(lines)

if __name__ == '__main__':
    report = generate_report('scanme.nmap.org', '1-200')
    print(report)

    with open('report.txt', 'w') as f:
        f.write(report)

Expected output:

Port Scan Report
Target: scanme.nmap.org
Generated: 2026-08-16T10:15:00
----------------------------------------
Host: 45.33.32.156 (scanme.nmap.org)
State: up
  22/tcp open - ssh OpenSSH 6.6.1p1
  80/tcp open - http Apache httpd 2.4.7
Total open TCP ports: 2

Small reporting scripts like this one are often the first genuinely “useful” artifact beginners build with python-nmap, bridging the gap between raw scanning and something that fits into an actual workflow.

When Simple Scanning Isn’t Enough

As projects grow, a few natural next steps extend beyond what’s covered here:

Frequently Asked Questions

Why does my scan return no results at all for a host I know is online? This is almost always caused by host discovery failing due to blocked ICMP. Add arguments='-Pn' to force Nmap to scan ports regardless of ping response.

Can python-nmap scan UDP ports? Yes, by passing arguments='-sU', though UDP scans are significantly slower and results are often less definitive (open|filtered) than TCP scans, exactly as with the Nmap command line.

Do I need root privileges to use python-nmap for basic scans? Not for the default TCP connect-style scanning behavior. Root/administrator privileges are only required when using scan types like SYN scanning (-sS) or OS detection (-O), same as with the raw Nmap CLI.

How do I scan only for a specific service, like just checking if SSH is open? Simply restrict the port range to that specific port: scanner.scan(target, '22'), then check the resulting state for port 22 directly.

Is python-nmap suitable for scanning thousands of hosts? It can be used for that scale, but for very large environments, consider combining it with asynchronous scanning (covered in the companion article on scanning modes) and reasonable concurrency limits to avoid overwhelming either the network or the scanning machine’s resources.

Conclusion

Simple port scanning with python-nmap is a great entry point into network security automation. With just a few lines of code, you can scan hosts, extract open ports and service details, and build reusable functions for larger projects. From here, the natural next steps are exploring the different scanning modes (synchronous, asynchronous, and generator-based) covered in the companion article, and eventually building full asset-discovery or monitoring tools on top of this foundation.

Exit mobile version