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
- Only scan hosts you own or are explicitly authorized to test —
scanme.nmap.orgis one of the very few hosts that is safe and legal to scan without prior arrangement. - Avoid scanning large ranges or the full port spectrum (
-p-) against production systems without coordinating with the system owner, since this can be noisy and resource-intensive. - When building scripts for others, hardcode or validate an allow-list of permitted target ranges to prevent accidental or malicious misuse.
- Store scan output securely, since it can reveal exploitable details about live infrastructure.
Limitations
- python-nmap doesn’t add scanning capability beyond what Nmap itself supports — it’s purely a convenience wrapper.
- Basic scans without
-sVwon’t return service/version details, only port state. - Very large scans can be memory-intensive since results are loaded into Python dictionaries rather than streamed.
- Privilege requirements for certain scan types apply exactly as they would on the command line — python-nmap can’t bypass OS-level permission requirements.
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:
- Scheduling recurring scans with cron (Linux/macOS) or Task Scheduler (Windows) to track changes over time.
- Adding a database layer (SQLite is a lightweight starting point) instead of flat files, once scan history needs to be queried rather than just archived.
- Moving to asynchronous or generator-based scanning modes once scanning many hosts sequentially becomes a bottleneck — covered in detail in the companion article on scanning modes with python-nmap.
- Layering in NSE vulnerability scripts via the
argumentsparameter once basic port/service enumeration is no longer sufficient for the task at hand.
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.
