Logo
Python Programming

Python Functions and Classes Explained

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

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: 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.
  • self refers to the specific instance. It's how the methods access the object's own data.
  • self.open_ports is 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

Functions vs classes: when to use which

This is the question most beginners get stuck on. Here's a practical heuristic.

Decision guide: function vs class in Python A flowchart with a starting question: Does the logic need to remember state between calls? If No, use a function. If Yes, ask: Do multiple functions share the same data? If Yes, use a class. If No, consider a function with a data container like a dict or dataclass. Function vs class: decision guide Does the logic need to remember state? Yes Do multiple functions share the same data? No Use a function Yes Use a class No Function + dict/dataclass
Figure 1. Start with functions. Reach for a class when you need to share state across multiple methods.

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.

← Back to all articles
{{ toast }}