python-nmap is a popular third-party Python library that wraps the Nmap binary and parses its XML output into convenient Python objects. Instead of manually building command-line strings and parsing raw text output, python-nmap gives developers a clean, Pythonic interface for running scans and working with results programmatically.
This guide covers the different scanning modes available through python-nmap — synchronous, asynchronous, and non-blocking scans — along with syntax, examples, expected output, use cases, and troubleshooting tips.
Installing python-nmap
pip install python-nmap
Note that Nmap itself must also be installed separately on the system, since python-nmap is only a wrapper around the actual Nmap binary — it doesn’t reimplement scanning functionality in pure Python.
nmap --version
Run this command to confirm Nmap is properly installed and accessible in the system PATH before testing any Python scripts.
Basic Setup
import nmap
scanner = nmap.PortScanner()
The PortScanner class is the core object used for most scanning tasks in python-nmap. It has methods for running scans and retrieving results in structured formats.
Scanning Mode 1: Synchronous Scanning
Synchronous scanning is the simplest mode — the script calls scan() and waits (blocks) until the scan completes before continuing.
import nmap
scanner = nmap.PortScanner()
scanner.scan('192.168.1.10', '22-443')
print(scanner.command_line())
print(scanner[('192.168.1.10')].state())
Expected output:
nmap -oX - -p 22-443 192.168.1.10
up
Retrieving Detailed Results
for host in scanner.all_hosts():
print(f"Host: {host} ({scanner[host].hostname()})")
print(f"State: {scanner[host].state()}")
for proto in scanner[host].all_protocols():
print(f"Protocol: {proto}")
ports = scanner[host][proto].keys()
for port in sorted(ports):
state = scanner[host][proto][port]['state']
print(f" Port {port}: {state}")
Expected output:
Host: 192.168.1.10 (example-host)
State: up
Protocol: tcp
Port 22: open
Port 80: open
Port 443: open
Synchronous scanning is best suited for simple scripts, small target lists, or situations where scan results are needed immediately before proceeding to the next step in a program.
Scanning Mode 2: Asynchronous Scanning
Asynchronous scanning uses the PortScannerAsync class, allowing a scan to run in the background while the main script continues executing. This is useful for scanning multiple targets without freezing the entire program, or for building responsive tools (like GUIs or web dashboards) that shouldn’t hang while a scan is in progress.
import nmap
import time
def scan_callback(host, scan_result):
print(f"Scan finished for {host}")
print(scan_result)
scanner = nmap.PortScannerAsync()
scanner.scan(hosts='192.168.1.10', arguments='-sV', callback=scan_callback)
while scanner.still_scanning():
print("Waiting for scan to complete...")
time.sleep(1)
print("Scan fully completed.")
Expected behavior:
Waiting for scan to complete...
Waiting for scan to complete...
Scan finished for 192.168.1.10
{'nmap': {...}, 'scan': {...}}
Scan fully completed.
The callback function is triggered automatically once the scan finishes, receiving the host and the full scan result dictionary. This design pattern is especially useful when scanning several hosts concurrently, each with its own async scan and callback.
Scanning Multiple Hosts Asynchronously
import nmap
import time
results = {}
def make_callback(target):
def callback(host, scan_result):
results[target] = scan_result
return callback
targets = ['192.168.1.10', '192.168.1.11', '192.168.1.12']
scanners = []
for target in targets:
scanner = nmap.PortScannerAsync()
scanner.scan(hosts=target, arguments='-sV', callback=make_callback(target))
scanners.append(scanner)
while any(s.still_scanning() for s in scanners):
time.sleep(1)
for target, result in results.items():
print(f"Results for {target}: {result}")
Scanning Mode 3: Non-Blocking Scanning with PortScannerYield
Some versions of python-nmap also support a generator-based approach that yields results progressively as hosts complete, rather than waiting for the entire scan or relying on callbacks.
import nmap
scanner = nmap.PortScannerYield()
for host, result in scanner.scan('192.168.1.0/28', arguments='-sV'):
print(f"Host: {host}")
print(result)
This mode is useful for scanning large subnets where you want to start processing each host’s results as soon as they’re available, rather than waiting for the full scan of the entire range to finish.
Choosing Scan Arguments
python-nmap passes any valid Nmap command-line flags through the arguments parameter, meaning virtually anything possible from the Nmap CLI is possible through the library.
scanner.scan('192.168.1.10', arguments='-sS -sV -O --script=vuln')
Common argument combinations:
# Fast scan of common ports
scanner.scan('192.168.1.10', arguments='-F')
# Full port range with service detection
scanner.scan('192.168.1.10', '1-65535', arguments='-sV')
# Aggressive scan
scanner.scan('192.168.1.10', arguments='-A')
# UDP scan
scanner.scan('192.168.1.10', arguments='-sU')
Note: Scan types requiring raw sockets, such as -sS (SYN scan) or -O (OS detection), require the Python process to run with elevated/root privileges, exactly as they would from the command line.
Scanning a Range of IPs or a Subnet
scanner.scan(hosts='192.168.1.0/24', arguments='-sn')
for host in scanner.all_hosts():
print(f"{host}: {scanner[host].state()}")
Using -sn performs a ping scan (host discovery only, no port scan), which is a quick way to inventory which hosts are alive on a subnet before running deeper scans against each one individually.
Working with Scan Results as Dictionaries
Every scan result in python-nmap can also be accessed as a raw dictionary via the .csv() method or by directly inspecting scanner._scan_result, which mirrors the structure of Nmap’s XML output.
csv_output = scanner.csv()
print(csv_output)
Expected output (comma-separated):
host;hostname;hostname_type;protocol;port;name;state;product;extrainfo;reason;version;conf;cpe
192.168.1.10;example-host;PTR;tcp;22;ssh;open;OpenSSH;;syn-ack;8.2p1;10;
192.168.1.10;example-host;PTR;tcp;80;http;open;Apache httpd;;syn-ack;2.4.41;10;
This CSV export is particularly convenient for quickly dumping results into spreadsheets or basic reporting pipelines without needing custom parsing logic.
Checking Nmap Version and Scan Statistics
print(scanner.nmap_version())
print(scanner.scanstats())
Expected output:
(7, 94)
{'timestr': '...', 'elapsed': '2.14', 'uphosts': '1', 'downhosts': '0', 'totalhosts': '1'}
scanstats() is useful for logging or dashboards that need to report how long a scan took and how many hosts were up versus down.
Error Handling
import nmap
scanner = nmap.PortScanner()
try:
scanner.scan('192.168.1.10', arguments='-sS')
except nmap.PortScannerError as e:
print(f"Scan error: {e}")
PortScannerError is raised for issues like Nmap not being installed, invalid arguments, or insufficient permissions for privileged scan types.
Choosing the Right Scanning Mode
| Mode | Class | Best For |
|---|---|---|
| Synchronous | PortScanner | Simple scripts, single target, immediate results needed |
| Asynchronous | PortScannerAsync | Multiple targets, background scanning, GUI/web apps |
| Non-blocking/Generator | PortScannerYield | Large subnets, progressive result processing |
Troubleshooting Common Issues
“nmap program was not found in path” Cause: Nmap binary isn’t installed or isn’t in the system PATH. Fix: Install Nmap and confirm with nmap --version from a terminal; on Windows, ensure the Nmap install directory is added to the PATH environment variable.
PermissionError or empty results on SYN scans Cause: Insufficient privileges for raw socket scan types. Fix: Run the Python script with elevated privileges (sudo on Linux/macOS) or switch to -sT for a TCP connect scan.
Async scan callback never triggers Cause: The main thread exits before the background scan completes. Fix: Use a while scanner.still_scanning(): time.sleep(1) loop to keep the main thread alive until scanning finishes.
KeyError when accessing scan results Cause: Attempting to access a host or port that wasn’t actually scanned or returned no data (e.g., host was down). Fix: Always check scanner.all_hosts() and scanner[host].state() before drilling into port-level data.
Security Best Practices
- Only scan hosts and networks you own or have explicit written authorization to test.
- Rate-limit scans (
--min-rate,--max-ratearguments passed througharguments=) when automating scans against production or shared infrastructure. - Store and log scan results securely, especially if they include service versions or vulnerability data that could itself be sensitive.
- Avoid embedding scan targets from untrusted or user-supplied input directly into the
argumentsstring, since python-nmap ultimately builds a command line — validate and sanitize any dynamic input first. - When building scanning tools for others to use, add explicit scope restrictions (e.g., an allow-list of subnets) to prevent misuse.
Limitations of python-nmap
- It is a thin wrapper, so it inherits every limitation of the underlying Nmap installation and version — advanced features depend on having a sufficiently recent Nmap binary installed.
- Asynchronous mode relies on threading and callbacks rather than modern
asyncio, which can feel dated compared to newer async Python patterns. - Error messages from failed scans are sometimes generic, requiring the caller to inspect Nmap’s own stderr output for the real cause.
- Large-scale, high-concurrency scanning across many hosts can become memory-intensive since full XML results are parsed into Python dictionaries in memory.
Combining Scanning Modes in a Real Project
Larger tools often combine modes depending on the situation — synchronous for quick single-host checks, asynchronous for background monitoring tasks. Here’s an example of a simple monitoring loop that periodically re-scans a set of hosts asynchronously and logs any changes in open ports:
import nmap
import time
import json
import os
STATE_FILE = "last_known_ports.json"
def load_previous_state():
if os.path.exists(STATE_FILE):
with open(STATE_FILE) as f:
return json.load(f)
return {}
def save_state(state):
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
def callback(host, scan_result):
previous_state = load_previous_state()
current_ports = []
try:
tcp_data = scan_result['scan'][host]['tcp']
current_ports = [port for port, data in tcp_data.items() if data['state'] == 'open']
except KeyError:
pass
previous_ports = previous_state.get(host, [])
if set(current_ports) != set(previous_ports):
print(f"Change detected on {host}: was {previous_ports}, now {current_ports}")
previous_state[host] = current_ports
save_state(previous_state)
scanner = nmap.PortScannerAsync()
scanner.scan(hosts='192.168.1.10', arguments='-sV', callback=callback)
while scanner.still_scanning():
time.sleep(1)
This kind of pattern is the foundation for basic change-detection monitoring tools — running this on a schedule (via cron or a task scheduler) creates a lightweight way to notice when new services appear on watched hosts.
Performance Considerations Across Modes
- Synchronous scanning is simplest but blocks the entire script, making it a poor fit for scanning many hosts sequentially within time-sensitive applications.
- Asynchronous scanning allows concurrency but is built on Python’s threading model, meaning very high host counts (hundreds or thousands) can still strain system resources if not managed carefully with limits on concurrent scanners.
- Generator-based scanning balances memory use reasonably well for large subnets since results are processed incrementally, but it still fundamentally waits on Nmap’s own scan speed under the hood — the Python layer doesn’t make Nmap itself scan faster.
In all cases, the actual bottleneck is almost always Nmap’s own network-level scanning speed, not the Python wrapper layer. Tuning goes further with Nmap-level flags (-T4, --min-rate) passed through arguments= than with Python-level concurrency alone.
Logging Scan Activity Across Modes
Regardless of which scanning mode is used, adding basic logging is good practice for any tool that will run repeatedly or be shared with a team. Python’s built-in logging module integrates cleanly with any of the three modes:
import nmap
import logging
logging.basicConfig(
filename='scan_activity.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
scanner = nmap.PortScanner()
target = '192.168.1.10'
logging.info(f"Starting scan of {target}")
scanner.scan(target, arguments='-sV')
logging.info(f"Scan complete. Command used: {scanner.command_line()}")
for host in scanner.all_hosts():
state = scanner[host].state()
logging.info(f"Host {host} is {state}")
This produces a persistent audit trail of what was scanned, when, and with what command — valuable both for troubleshooting automation issues later and for demonstrating accountability in professional environments where scanning activity needs to be documented.
Integrating Scan Modes into a Larger Application
In practice, scanning modes rarely exist in isolation — they’re usually one component of a larger tool, such as a Flask or Django web dashboard that triggers scans and displays results. A minimal example of triggering an asynchronous scan from a web request handler illustrates the pattern:
import nmap
import threading
scan_status = {}
def background_scan(target):
scanner = nmap.PortScanner()
scanner.scan(target, arguments='-sV')
scan_status[target] = {
'complete': True,
'results': scanner[target] if target in scanner.all_hosts() else None
}
def start_scan(target):
scan_status[target] = {'complete': False, 'results': None}
thread = threading.Thread(target=background_scan, args=(target,))
thread.start()
return "Scan started"
This pattern — kicking off a scan in a background thread and polling a status dictionary — is a common lightweight approach before reaching for a full task queue system like Celery in larger production applications.
Frequently Asked Questions
Which scanning mode should a beginner start with? Synchronous scanning via PortScanner. It’s the simplest to reason about and matches how most people first learn to use Nmap from the command line.
Can I cancel a scan that’s already running with python-nmap? python-nmap doesn’t provide a clean built-in cancellation method for in-progress scans. In practice, developers work around this by running scans in a separate process (via multiprocessing) that can be terminated externally, or by scoping scans tightly enough that cancellation is rarely needed.
Does python-nmap support scanning with sudo/root privileges from within a script? It doesn’t elevate privileges itself — if the underlying scan type requires root/administrator access (like -sS or -O), the Python process itself needs to be run with those privileges.
Is asynchronous scanning in python-nmap the same as Python’s asyncio? No. PortScannerAsync uses threading under the hood, not asyncio coroutines. For projects already built around asyncio, wrapping python-nmap calls in an executor (loop.run_in_executor) is a common integration pattern.
Can scan results be directly converted to JSON? Yes — since results are already Python dictionaries, json.dumps(scanner._scan_result) or manually building a clean dictionary (as shown in the companion article on simple port scanning) both work well for exporting to JSON.
Conclusion
python-nmap offers three distinct scanning modes — synchronous, asynchronous, and generator-based — each suited to different automation needs. Synchronous scanning covers most simple use cases, asynchronous scanning is ideal for responsive applications or scanning multiple targets concurrently, and the generator-based approach handles large subnets efficiently by yielding results progressively. Understanding which mode fits a given project, combined with sensible error handling and responsible scanning practices, makes python-nmap a powerful building block for custom network security tooling.