Python Functions and Classes Explained

Short answer: Functions are reusable blocks of code that perform a single task. Classes are blueprints for creating objects that bundle related data and functions together. For most IT and security scripting, you'll write functions from day one and reach for classes once your scripts grow complex enough to benefit from grouping related state and behaviour.
TL;DR: Learn functions first — they're required from your first real script. Classes are the next step: they group related data and behaviour into a reusable structure. Use a class when your functions start sharing the same data through multiple parameters.
Key Takeaways - A function is defined with
def, takes inputs, performs work, and optionally returns a value. - A class is a blueprint; an instance (object) is a specific thing built from that blueprint. - Functions are the right tool for single, well-defined tasks. Classes are the right tool when tasks share state — data that needs to persist across calls. - Python's standard library is built on both — understanding them lets you use any library more effectively. - The official Python docs cover classes in full detail once you've worked through the basics here.
Functions: the building block you use every day

Every useful Python script contains functions. They do one thing, they're named clearly, and they can be called from anywhere in your code without rewriting the logic. If you're applying these concepts in a security or IT context, Python automation tasks for IT professionals shows how functions like these appear in real working scripts.
Defining a function
Use the def keyword, give the function a name, list its parameters in parentheses, and write the body indented below.
def sanitize_ip(ip: str) -> str:
"""Strip whitespace and validate basic IP format."""
return ip.strip()
This function takes one string argument, trims whitespace, and returns the cleaned string. That's it — simple, testable, reusable.
Parameters and return values
Functions receive input through parameters and send output back with return. If you don't include a return statement, Python returns None implicitly.
def count_failed_logins(log_lines: list, username: str) -> int:
"""Count failed login attempts for a specific username in a list of log lines."""
count = 0
for line in log_lines:
if "Failed password" in line and username in line:
count += 1
return count
Call it like this:
with open("/var/log/auth.log") as f:
lines = f.readlines()
failures = count_failed_logins(lines, "admin")
print(f"Failed logins for admin: {failures}")
Default parameters and keyword arguments
Parameters can have default values. Callers can override them by name (keyword arguments) or leave them at the default.
def build_alert_message(event: str, severity: str = "medium", source_ip: str = "unknown") -> str:
"""Build a formatted alert message string."""
return f"[{severity.upper()}] {event} | Source: {source_ip}"
# Use defaults
print(build_alert_message("Port scan detected"))
# → [MEDIUM] Port scan detected | Source: unknown
# Override severity and source
print(build_alert_message("Brute force attempt", severity="high", source_ip="10.0.0.5"))
# → [HIGH] Brute force attempt | Source: 10.0.0.5
When to write a function
Write a function whenever you catch yourself repeating the same code. A good rule: if you've written the same three lines twice, wrap them in a function.
Classes: bundling data and behaviour

Classes become useful when your functions start sharing the same data. Instead of passing the same variables into every function, a class keeps that data in one place and attaches the relevant functions to it.
The anatomy of a class
class HostScanner:
"""Encapsulates host scanning state and methods."""
def __init__(self, target_host: str, timeout: int = 3):
self.target = target_host
self.timeout = timeout
self.open_ports = []
self.scan_completed = False
def scan_port(self, port: int) -> bool:
"""Return True if the port is open on the target host."""
import socket
try:
with socket.create_connection((self.target, port), timeout=self.timeout) as _:
return True
except (socket.timeout, ConnectionRefusedError, OSError):
return False
def scan_range(self, start_port: int, end_port: int) -> list:
"""Scan a range of ports and store open ones. Returns list of open ports."""
self.open_ports = []
for port in range(start_port, end_port + 1):
if self.scan_port(port):
self.open_ports.append(port)
self.scan_completed = True
return self.open_ports
def report(self) -> str:
"""Return a human-readable summary of scan results."""
if not self.scan_completed:
return f"No scan run yet for {self.target}."
if not self.open_ports:
return f"{self.target}: no open ports found in scanned range."
ports_str = ", ".join(str(p) for p in self.open_ports)
return f"{self.target}: open ports — {ports_str}"
What each piece does:
__init__is the constructor — it runs when you create an instance and sets up the object's initial state.selfrefers to the specific instance. It's how the methods access the object's own data.self.open_portsis an instance attribute — data that belongs to this particular scanner.
Using the class:
scanner = HostScanner("192.168.1.1", timeout=2)
scanner.scan_range(1, 1024)
print(scanner.report())
Notice that scan_range and report don't need you to pass target or open_ports — the class holds that state. That's the point.
__init__, methods, and attributes
| Term | What it means | Example |
|---|---|---|
| Class | The blueprint/template | class HostScanner: |
| Instance | A specific object created from the class | scanner = HostScanner("10.0.0.1") |
| Attribute | Data stored on an instance | self.target, self.open_ports |
| Method | A function defined inside a class | def scan_port(self, port) |
__init__ |
Constructor — runs on object creation | Sets up initial state |
self |
Reference to the current instance | Lets methods access instance data |
Functions vs classes: when to use which

This is the question most beginners get stuck on. Here's a practical heuristic.
When your data fits a single class with no complex behavior, a dataclass is often the right middle ground — it gives you structured attributes without the overhead of a full class definition.
| Scenario | Use a function | Use a class |
|---|---|---|
| Parse one log file | Yes | No |
| Reusable IP validation | Yes | No |
| Port scanner with persistent results | No | Yes |
| API client with auth headers | No | Yes |
| Hash a single file | Yes | No |
| Multi-step report builder | No | Yes |
| One-off data transformation | Yes | No |
Putting it together: a practical security example
Here's a pattern that appears constantly in security tooling — a class that holds configuration and results, with methods that do the work.
from pathlib import Path
from datetime import datetime
import hashlib
class FileAuditor:
"""Audit files in a directory for suspicious properties."""
SUSPICIOUS_EXTENSIONS = {".exe", ".bat", ".ps1", ".sh", ".py", # Python scripts in upload dirs can be dropper scripts
".vbs"}
def __init__(self, directory: str):
self.directory = Path(directory)
self.results = []
self.scanned_at = None
def hash_file(self, file_path: Path) -> str:
"""Return SHA-256 hash of a file."""
sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest()
def is_suspicious(self, file_path: Path) -> bool:
"""Return True if the file has a suspicious extension."""
return file_path.suffix.lower() in self.SUSPICIOUS_EXTENSIONS
def audit(self) -> list:
"""Scan directory and return list of suspicious file findings."""
self.results = []
self.scanned_at = datetime.now().isoformat()
for file_path in self.directory.rglob("*"):
if file_path.is_file() and self.is_suspicious(file_path):
self.results.append({
"path": str(file_path),
"extension": file_path.suffix,
"size_bytes": file_path.stat().st_size,
"sha256": self.hash_file(file_path),
})
return self.results
def summary(self) -> str:
"""Return a one-line summary of audit results."""
if self.scanned_at is None:
return "No audit run yet."
return (
f"Audit of {self.directory} at {self.scanned_at}: "
f"{len(self.results)} suspicious file(s) found."
)
# Usage
auditor = FileAuditor("/tmp/uploads")
auditor.audit()
print(auditor.summary())
for finding in auditor.results:
print(f" {finding['path']} — {finding['sha256'][:16]}...")
For more on applying these patterns to automation workflows, see Python automation tasks for IT professionals. For networking-specific patterns, the Python networking basics guide builds directly on these class and function concepts.
Frequently asked questions
What is the difference between a function and a method in Python?
A method is a function defined inside a class. The key difference is that methods receive self as their first parameter, giving them access to the instance's data. Outside a class, the same logic would just be a regular function. The terms are often used loosely, but technically: methods belong to objects, functions are standalone.
Do you need to use classes to write good Python?
No. Many excellent, production-quality Python scripts use only functions. Classes are a tool for managing complexity — specifically, shared state across related operations. If your scripts are short and task-focused, plain functions are perfectly appropriate. Reach for classes when you find yourself passing the same three or four variables into every function you write.
What does self mean in a Python class?
self is a reference to the specific instance of the class on which the method is being called. It lets each method access and modify that instance's attributes. Python passes self automatically — you declare it as the first parameter but never pass it explicitly when calling a method. It's a convention, not a keyword; you could name it anything, but PEP 8 recommends self as the first argument of instance methods, and it's used universally.
How do you know when a script is complex enough to need classes?
A practical signal: when you have three or more functions that all take the same parameter (like a hostname, a file path, or a configuration object), that shared data is a candidate to become a class attribute. Another signal: when functions need to call each other and share results. If your script fits in 50 lines with clean functions, classes likely aren't needed yet.
ThreatBlock's Python programming course teaches Python from first principles with a cybersecurity application focus.
About the author: Samaksh is Lead Instructor at ThreatBlock and holds the OSCP, CRTP, CRTO, and CEH certifications, with hands-on experience in penetration testing and red teaming. Read more on the author page · Editorial policy.
