Before dedicated libraries like python-nmap existed, and even now for many lightweight automation tasks, developers reach for Python’s built-in os and subprocess modules to run Nmap commands directly from a script. This approach gives full control over exactly how Nmap is invoked, what arguments are passed, and how output is captured — without depending on a third-party wrapper library.
This guide walks through both approaches, compares their strengths and weaknesses, and shows practical, working examples for automating Nmap scans from Python.
Why Automate Nmap with Python at All?
Running Nmap manually works fine for one-off scans, but automation becomes valuable when:
- Scanning needs to run on a schedule (e.g., nightly asset discovery)
- Results need to feed into another system (a dashboard, database, or alerting pipeline)
- Multiple targets need to be scanned with slightly different logic per target
- Scan output needs to be parsed, filtered, or transformed programmatically
Python is a natural fit here because of its readability and the availability of both low-level (os, subprocess) and high-level (python-nmap) tools for interacting with system commands.
Method 1: Using the os Module
The os module is the oldest way to run shell commands from Python. It’s simple but limited, and modern Python code generally avoids it in favor of subprocess. Still, it’s worth understanding since it appears in a lot of legacy scripts.
os.system()
import os
command = "nmap -sV 192.168.1.10"
os.system(command)
os.system() runs the command in a subshell and prints output directly to the terminal. It returns only the exit status code, not the actual scan output, which makes it unsuitable for any script that needs to process results.
import os
exit_code = os.system("nmap -sV 192.168.1.10")
print(f"Command exited with code: {exit_code}")
os.popen() (Deprecated but still seen in older code)
import os
stream = os.popen("nmap -sV 192.168.1.10")
output = stream.read()
print(output)
os.popen() at least allows capturing output as a string, unlike os.system(). However, it’s considered deprecated in favor of subprocess, offers weaker error handling, and doesn’t give fine control over stdin/stdout/stderr separately.
Limitations of the os Module
- No native way to separate standard output from standard error
- No built-in timeout handling
- Vulnerable to shell injection if command strings are built from untrusted input
- No structured way to check success/failure beyond a raw exit code
Because of these limitations, subprocess is the recommended approach for anything beyond quick, throwaway scripts.
Method 2: Using the subprocess Module
The subprocess module, introduced to replace os.system() and os.popen(), is the modern, recommended way to run external commands from Python, including Nmap.
Basic Usage with subprocess.run()
import subprocess
result = subprocess.run(
["nmap", "-sV", "192.168.1.10"],
capture_output=True,
text=True
)
print(result.stdout)
Key points about this example:
- Passing the command as a list (
["nmap", "-sV", "192.168.1.10"]) instead of a single string avoids shell injection risks and handles argument spacing correctly. capture_output=Truecaptures both stdout and stderr.text=Truedecodes output as a string instead of raw bytes.
Checking for Errors
import subprocess
result = subprocess.run(
["nmap", "-sV", "192.168.1.10"],
capture_output=True,
text=True
)
if result.returncode == 0:
print("Scan completed successfully")
print(result.stdout)
else:
print("Scan failed")
print(result.stderr)
Setting a Timeout
Long scans (like full port range scans) can hang. subprocess.run() supports a timeout parameter to prevent scripts from stalling indefinitely.
import subprocess
try:
result = subprocess.run(
["nmap", "-p-", "192.168.1.10"],
capture_output=True,
text=True,
timeout=300 # 5 minutes
)
print(result.stdout)
except subprocess.TimeoutExpired:
print("Scan timed out after 5 minutes")
Saving Output Directly to a File
import subprocess
with open("scan_output.txt", "w") as outfile:
subprocess.run(
["nmap", "-sV", "192.168.1.10"],
stdout=outfile,
stderr=subprocess.STDOUT
)
This writes Nmap’s live output directly to a file rather than holding it all in memory, which is helpful for very large scans.
Running Nmap with XML Output for Parsing
import subprocess
import xml.etree.ElementTree as ET
subprocess.run(
["nmap", "-sV", "-oX", "scan.xml", "192.168.1.10"],
capture_output=True,
text=True
)
tree = ET.parse("scan.xml")
root = tree.getroot()
for host in root.findall("host"):
address = host.find("address").get("addr")
print(f"Host: {address}")
for port in host.findall(".//port"):
port_id = port.get("portid")
state = port.find("state").get("state")
service = port.find("service").get("name") if port.find("service") is not None else "unknown"
print(f" Port {port_id}/{state} - {service}")
Expected output:
Host: 192.168.1.10
Port 22/open - ssh
Port 80/open - http
Port 443/open - https
Parsing Nmap’s XML output rather than screen-scraping plain text is far more reliable, since the XML schema is well-documented and stable across versions.
Scanning Multiple Targets in a Loop
import subprocess
targets = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]
for target in targets:
print(f"Scanning {target}...")
result = subprocess.run(
["nmap", "-sV", target],
capture_output=True,
text=True
)
print(result.stdout)
print("-" * 40)
For faster execution across many targets, this can be combined with Python’s concurrent.futures module to run scans in parallel, though care should be taken not to overwhelm the network or trigger IDS alerts with too many simultaneous scans.
import subprocess
from concurrent.futures import ThreadPoolExecutor
targets = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]
def scan(target):
result = subprocess.run(
["nmap", "-sV", target],
capture_output=True,
text=True
)
return target, result.stdout
with ThreadPoolExecutor(max_workers=3) as executor:
for target, output in executor.map(scan, targets):
print(f"Results for {target}:\n{output}\n")
Handling Privileges
Certain Nmap scan types (like SYN scans, -sS) require raw socket access, which typically means the Python script itself needs elevated privileges.
import subprocess
import os
if os.geteuid() != 0:
print("This script requires root privileges for SYN scans. Try running with sudo.")
else:
subprocess.run(["nmap", "-sS", "192.168.1.10"])
Note: os.geteuid() is Unix-specific and won’t work on Windows; Windows scripts would need a different privilege check, such as attempting the scan and catching the resulting error, or checking for administrator rights via the ctypes module.
Security Considerations When Automating Nmap
- Never build command strings via string concatenation with user input. Always pass arguments as a list to
subprocess.run()to avoid shell injection vulnerabilities. - Avoid
shell=Trueunless absolutely necessary — it reintroduces the injection risks that using a list of arguments is meant to prevent. - Validate target input before passing it to Nmap, especially if targets come from a web form, API, or external file, to prevent abuse of your automation as a scanning proxy for unauthorized targets.
- Log every automated scan with timestamp, target, and initiating user/process for audit purposes, particularly in enterprise environments.
- Respect authorization scope — automated scanning tools are easy to misuse at scale, so make sure any script includes safeguards against scanning out-of-scope targets (e.g., an explicit allow-list of subnets).
Troubleshooting Common Issues
“FileNotFoundError: [Errno 2] No such file or directory: ‘nmap'” Cause: Nmap isn’t installed or isn’t in the system’s PATH. Fix: Install Nmap and verify with nmap --version in a terminal; on Windows, ensure the install directory is added to PATH.
Script hangs indefinitely Cause: No timeout set on a scan targeting an unresponsive host or extremely large port range. Fix: Always set a timeout parameter in subprocess.run().
Permission denied errors on SYN scans Cause: Insufficient privileges for raw socket operations. Fix: Run the script with elevated privileges, or switch to -sT (TCP connect scan), which doesn’t require raw sockets.
Garbled or incomplete output Cause: Reading from stdout before the process has fully completed, often from misusing Popen directly instead of run(). Fix: Use subprocess.run() for simpler cases, which waits for the process to complete before returning, or properly use communicate() when using Popen directly.
os vs subprocess: Quick Comparison
| Feature | os.system() | os.popen() | subprocess.run() |
|---|---|---|---|
| Captures output | No | Yes (basic) | Yes (full control) |
| Separates stdout/stderr | No | No | Yes |
| Timeout support | No | No | Yes |
| Injection-safe (list args) | No | No | Yes |
| Recommended for new code | No | No | Yes |
When to Use python-nmap Instead
For more complex projects, the third-party python-nmap library (covered in the companion articles on scanning modes and simple port scanning with python-nmap) wraps around Nmap’s XML output automatically and provides a Pythonic object interface. The os/subprocess approach is best suited for:
- Simple, one-off automation scripts
- Situations where you want zero external dependencies
- Full control over exact command-line flags without an abstraction layer in the way
Meanwhile, python-nmap is better suited for larger projects that need structured, repeated access to scan results as Python objects rather than raw text or manually parsed XML.
Using subprocess.Popen() for Real-Time Output Streaming
subprocess.run() waits for the entire command to finish before returning anything, which isn’t ideal for long scans where you’d like to see progress as it happens. subprocess.Popen() gives more granular control, allowing output to be read line by line while the scan is still running.
import subprocess
process = subprocess.Popen(
["nmap", "-sV", "-p-", "192.168.1.10"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
for line in process.stdout:
print(line, end="")
process.wait()
print(f"\nProcess finished with exit code: {process.returncode}")
This pattern is especially useful when building a script that needs to show live progress in a terminal UI or log scan progress to a file in near real-time, rather than waiting silently until the entire scan completes.
Building a Small CLI Wrapper Script
A practical use of subprocess is wrapping Nmap into a lightweight command-line tool tailored to a specific team’s workflow, adding logging and consistent output handling on top of the raw Nmap invocation.
import subprocess
import argparse
import datetime
def run_scan(target, ports, output_file):
timestamp = datetime.datetime.now().isoformat()
print(f"[{timestamp}] Starting scan of {target} on ports {ports}")
result = subprocess.run(
["nmap", "-sV", "-p", ports, "-oN", output_file, target],
capture_output=True,
text=True
)
if result.returncode == 0:
print(f"Scan complete. Results saved to {output_file}")
else:
print(f"Scan failed: {result.stderr}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Simple Nmap wrapper")
parser.add_argument("target", help="Target host or IP")
parser.add_argument("--ports", default="1-1000", help="Port range to scan")
parser.add_argument("--output", default="scan_output.txt", help="Output file")
args = parser.parse_args()
run_scan(args.target, args.ports, args.output)
Usage from the command line:
python3 nmap_wrapper.py 192.168.1.10 --ports 1-1000 --output results.txt
This kind of wrapper is a common starting point for internal security tooling — adding consistent logging, output naming conventions, and argument validation around raw Nmap invocations without the overhead of a full third-party library.
Comparing os/subprocess to Higher-Level Automation
It’s worth being explicit about the trade-off being made when choosing os/subprocess over a wrapper library like python-nmap:
- Control:
subprocessgives complete control over exactly what command is run and how output is handled, with no abstraction hiding details. - Parsing burden: Nothing structures the output automatically — if you want structured data, you either parse XML manually (as shown earlier) or resort to fragile text parsing of the plain output.
- Dependency footprint:
subprocessrequires zero extra packages, which matters in locked-down environments where installing third-party Python packages isn’t straightforward. - Learning value: Understanding the raw command construction and output handling makes it much easier to debug issues later, even if a higher-level library is eventually adopted for a larger project.
Frequently Asked Questions
Why shouldn’t I use os.system() for anything beyond quick scripts? Because it offers no way to capture output as a string, no separation of stdout/stderr, no timeout support, and is more vulnerable to shell injection when building commands from variables — all of which subprocess addresses directly.
Is it safe to pass user input into subprocess commands? Only if it’s passed as a separate list element (never concatenated into a single string) and validated first. Even with list-based arguments, unchecked target input can allow your automation to be used against unauthorized systems, so validation logic matters as much as injection safety.
Can I run Nmap scans in parallel with subprocess? Yes, using either concurrent.futures.ThreadPoolExecutor (shown earlier) or by launching multiple Popen processes and tracking them independently. Be mindful of network load and IDS detection risk when parallelizing scans against shared infrastructure.
Does subprocess work the same way on Windows and Linux? Mostly yes, though path handling, privilege elevation checks (like os.geteuid()), and some default behaviors differ. Testing automation scripts on the actual target operating system before relying on them is good practice.
Should I parse Nmap’s plain text output or XML output? Always prefer XML (-oX) for programmatic parsing. Plain text output formatting can change subtly between Nmap versions, while the XML schema is stable and specifically designed for machine consumption.
Conclusion
Python’s os and subprocess modules provide a dependency-free way to integrate Nmap into custom automation scripts. While os.system() and os.popen() still show up in older code, subprocess.run() is the modern standard, offering better control over output capture, error handling, and timeouts, plus protection against shell injection when arguments are passed as a list. Understanding this low-level approach also makes it easier to appreciate what higher-level libraries like python-nmap are doing under the hood — ultimately, they’re running the same Nmap binary, just with a more convenient interface wrapped around it.
