Foundations of Ethical Security Testing with Python: A Beginner's Laboratory Guide
Chapter 1 of 11 · updated Jul 08, 2026
Python for Security: First Steps in Programming
Installing Python 3.12 and Verifying Your Setup
I still remember the first time I tried to follow a security tutorial that assumed Python was already "just there." Three hours of path-variable troubleshooting later, I abandoned it. We are not doing that to you. Lab Wintermute does not exist yet in your machine—we build it page by page—but every tool we use later, from port scanning scripts to log parsers, runs on Python. So we start with the foundation.
Download Python 3.12 from python.org. The exact navigation changes, so look for the stable release marked 3.12.x. During installation, check the box that adds Python to your system PATH; without this, your terminal will claim python does not exist even when it sits right there on your drive. On Linux distributions, your package manager likely offers 3.12 or close—check with your system's current repositories. On macOS, the python.org installer or Homebrew both work; I have used both and prefer the installer for beginners because it includes IDLE, which we use below.
Open a terminal—Command Prompt on Windows, Terminal on macOS, your shell of choice on Linux—and verify:
python --version
or, on many Linux systems where python still points to an older release:
python3 --version
Illustrative output — verify on your target:
Python 3.12.xIf you see 3.11 or 3.13, most of this guide still works, but 3.12 is what I tested against. If you see "command not found," your PATH needs attention or the install did not complete.
IDLE, Python's built-in editor, ships with the standard install. Find it in your applications menu. I use IDLE when teaching because it shows immediate feedback: type 2 + 2, press Enter, see 4. No compilation, no mystery. But for real work—everything we do from page five onward—we write scripts in text files and run them from terminal. IDLE is your training wheels, not your bicycle.
The Interpreter: Your First Conversation with Python
Open IDLE. You see a window with >>> prompts. This is the REPL (Read-Eval-Print Loop): it reads what you type, evaluates it, prints the result, loops forever. Type this exactly:
>>> print("Lab Wintermute initialized")
The string "Lab Wintermute initialized" appears below. print() is a function—a named block of code that performs an action. The parentheses hold its argument, the value it operates on. Strings (sequences of text characters) require quotation marks so Python knows where they start and end.
Now type something that fails on purpose:
>>> print(Lab Wintermute initialized)
You get a NameError: name 'Lab' is not defined. Python tried to treat Lab as a variable name, not text, and found nothing assigned to it. This is your first traceback—Python's way of showing where and why it broke. Tracebacks read bottom-up: the last line tells you the error type and human-readable complaint; lines above show the file and line number where execution failed. Reading tracebacks is not optional polish; it is how you debug when your port scanner hits a closed port and crashes instead of handling it gracefully. We will practice this deliberately.
Variables: Named Containers for Data
A variable stores a value under a name you choose. In security work, you name your target host, your port list, your timeout values. Type these in IDLE:
>>> target_host = "192.0.2.20"
>>> target_port = 22
>>> scan_timeout = 3.5
>>> is_authorized = True
target_host holds a string (str), text data. target_port holds an integer (int), a whole number. scan_timeout holds a float, a number with decimal precision. is_authorized holds a boolean (bool), either True or False. The = sign assigns; it does not mean mathematical equality here.
Verify what you stored:
>>> type(target_host)
<class 'str'>
>>> type(target_port)
<class 'int'>
Python is dynamically typed—you do not declare types in advance, but the type matters. You cannot add a string to an integer directly; target_host + target_port raises a TypeError. This frustrates newcomers but protects you from subtle bugs, like concatenating a port number into a URL incorrectly.
Lists store multiple values in order. We use them for target port ranges:
>>> common_ports = [22, 80, 443, 3306, 8080]
>>> common_ports[0]
22
Indexing starts at 0, not 1. common_ports[0] is the first element. This trips nearly everyone until it becomes muscle memory.
Your First Script: hello_security.py
IDLE is for experiments. Scripts are for keeping and sharing work. Open any plain text editor—Notepad works, VS Code is better, avoid Word because it adds invisible formatting characters that break Python.
Create a new file. Save it as hello_security.py in a dedicated folder for this guide. The .py extension tells your system this is Python code. Type exactly:
# hello_security.py
# Samuel Segato, DeafNews — Lab Wintermute training material
# My first security script: prints authorization reminder and target info target_host = "192.0.2.20" # Metasploitable2 server IP (authorized lab target)
target_role = "vulnerable server" # What this host represents in our scenario
is_authorized = True # CRITICAL: never run scans without written authorization print("=== Lab Wintermute Session ===")
print("Target:", target_host)
print("Role:", target_role)
print("Authorization confirmed:", is_authorized) if not is_authorized: print("ABORT: No authorization. Exiting.")
else: print("Proceeding with authorized reconnaissance only.")
Lines starting with # are comments. Python ignores them. They exist for humans: your future self, your lab partner, the senior reviewer who checks your work. In security, comments document authorization status, scope boundaries, and why a particular target was selected. I have seen junior testers criticized more for missing comments than for technical errors, because uncommented offensive code looks reckless.
Run your script from terminal. Navigate to your script's folder, then:
python hello_security.py
or python3 hello_security.py as your system requires.
Illustrative output — verify on your target:
=== Lab Wintermute Session === Target: 192.0.2.20 Role: vulnerable server Authorization confirmed: True Proceeding with authorized reconnaissance only.
If you see a SyntaxError or IndentationError, check that you used spaces consistently—Python cares about indentation, unlike many languages. Four spaces per level is convention. IDLE helps; bare Notepad does not.
Functions: Reusable Code Blocks
Copy-pasting the same scan logic for ten targets is unmaintainable. Functions wrap reusable logic. Add this to your script above the existing code, or create check_port.py:
# check_port.py
# Stub for port connectivity check — full implementation in later pages import socket # Python's built-in networking module def test_target_connectivity(host, port): """ Attempt TCP connection to host:port. Returns True if connection succeeds, False otherwise. This is a stub — real timeout and error handling come later. """ try: # socket.AF_INET = IPv4 addressing # socket.SOCK_STREAM = TCP (connection-oriented) protocol s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = s.connect_ex((host, port)) # connect_ex returns error code, not exception s.close() return result == 0 # 0 means success except socket.error as e: print(f"Socket error for {host}:{port}: {e}") return False # Test against our authorized lab target
test_host = "192.0.2.20"
test_port = 22 print(f"Testing {test_host}:{test_port}...")
if test_target_connectivity(test_host, test_port): print("Port appears open (or accepting connections)")
else: print("Port closed, filtered, or host unreachable")
import socket brings in Python's standard library module for network operations. The socket module, per its documented behavior, provides socket() returning an object whose methods implement system-level networking calls. For IPv4 (AF_INET), addresses are (host, port) pairs with host as string and port as integer—exactly why we defined test_port = 22 as a number, not "22".
⚠️ Authorized, defensive use only. This script connects to a single host-port pair. Running it against arbitrary internet addresses without permission violates computer fraud laws in most jurisdictions. Lab Wintermute exists precisely so you learn the mechanism without legal exposure.
The try / except structure catches errors gracefully. Without it, a refused connection raises an exception and crashes your script. In security automation, crashes are information leaks—they reveal your tool's presence and stop your data collection. Handling socket.error lets you log the failure and continue.
Run it:
python check_port.py
Illustrative output — verify on your target:
Testing 192.0.2.20:22... Port appears open (or accepting connections)or, if Metasploitable2's SSH service is down or your network is misconfigured:
Testing 192.0.2.20:22... Port closed, filtered, or host unreachable
This is intentionally a stub. Pages five and six expand it with proper timeout handling, multiple port iteration, and result formatting. But the skeleton matters: you now see how a minimal security tool is structured—import, define function, handle errors, execute with test data.
Reading Tracebacks: The Essential Debugging Skill
Break your script deliberately. Change socket.socket(socket.AF_INET, socket.SOCK_STREAM) to socket.socket(socket.AF_INET, "SOCK_STREAM")—passing a string where an integer constant is expected. Run it.
Illustrative traceback — your exact line numbers will differ:
Traceback (most recent call last): File "check_port.py", line 24, in <module> if test_target_connectivity(test_host, test_port): File "check_port.py", line 13, in test_target_connectivity s = socket.socket(socket.AF_INET, "SOCK_STREAM") TypeError: 'str' cannot be interpreted as an integer
Read from bottom: TypeError tells you the category—wrong data type used. The message explains. The line above shows the failing code. The lines above that show the call stack—who called whom to get here. Fix the error, rerun, confirm clean execution. This loop—break, read, fix, verify—is how you develop any non-trivial tool. I still do this twenty times a day.
Structure and Readability for Collaborative Work
Security is rarely solo. Your script may be reviewed by a senior, handed to a teammate, or submitted as coursework. Structure matters as much as function.
Conventions I follow and enforce in my lab:
- Comments at top: filename, author, purpose, authorization status
- Constants in UPPER_CASE if truly constant (rare in beginner scripts; we use regular variables)
- Functions do one thing:
test_target_connectivityonly tests; it does not print reports or write files - Main execution guarded: wrap test calls in
if __name__ == "__main__":so importing your script as a module does not accidentally run it
Add this guard to check_port.py by indenting the bottom test code:
if __name__ == "__main__": test_host = "192.0.2.20" test_port = 22 # ... rest of execution block
__name__ == "__main__" is True only when the file is run directly, not when imported. This prevents surprises when we reuse check_port.py as a module in larger tools later.
Checkpoint: What You Should Have Now
| Artifact | Location | Purpose |
|---|---|---|
hello_security.py | Your guide folder | Authorization reminder; demonstrates variables, print, basic conditional |
check_port.py | Same folder | First security-relevant stub; demonstrates functions, import socket, error handling, structure |
Both scripts run without error against Lab Wintermute's defined targets. You can modify target_host to 192.0.2.30 (DVWA) and test_port to 80 to verify the web server responds. You understand why 192.0.2.10 is your Kali workstation—not a target—and why is_authorized exists even when always True in training.
In plain terms: you have learned to make Python remember data, do math and logic, talk to networks safely, and tell you when it breaks. These four skills repeat in every security tool you will write.
Page two covers more syntax—loops, dictionaries, file reading—then immediately applies them to parsing network logs and building target lists. The check_port.py stub returns there, expanded into something you could actually use for authorized reconnaissance documentation.