Session 5 — Chapter 8: Classes, Instances, Encapsulation, Inheritance, Modules & Standard Libraries
SEC1302 · Python for Information Security
>>> class SecurityContext:
... def __init__(self, principal: str, role: str):
... self.principal = principal
... self.role = role
... def evaluate_access(self, target_resource: str) -> bool:
... return self.role == "sec_admin" or target_resource.startswith("public_")
...
>>> session = SecurityContext("audit_agent", "sec_admin")
>>> session.evaluate_access("firewall_policy_table")
True1
8.1 Creating and Using a Class
8.2 Working with Classes and Instances
8.3 Inheritance & Object Composition
8.4 Storing and Importing Classes in Modules
8.5 The Python Standard Library & Scientific Computing
2
Define classes using the class keyword, __init__() constructor, self parameter, and encapsulate state and behavior into concrete objects.
Implement default attribute values, manage attribute modification via guarded setter methods, and enforce data invariants defensively.
Implement single inheritance with super(), override parent behaviors polymorphically, and decompose complex systems using object composition.
Distribute classes across external modules cleanly, evaluate pseudo-random vs. cryptographically secure generators, and utilize statistics, NumPy, and Pandas for telemetry analysis.
Mapped to CLO1 — apply object-oriented design and Python modular architecture to security automation tools and structured telemetry processing (C3-P2).
3
Object-Oriented Programming (OOP) is a paradigm that models software around data structures called objects rather than actions or logic alone.
In procedural programming (Chapters 1–7), programs are organized into functions operating on external, decoupled data structures.
In OOP, a class bundles data (attributes representing state) and functions (methods representing behavior) into a cohesive unit.
An individual concrete realization created from a class is termed an instance or object.
4
The class keyword instructs Python to create a new class definition.
By convention in PEP 8, class names use CapWords (PascalCase), distinguishing them from lowercase function and variable names.
A class definition acts as a blueprint or schema specifying what information instances will store and what actions they can perform.
Parentheses are omitted after the class name when creating a class from scratch without inheritance.
The docstring immediately beneath the class header defines its architectural responsibility.
class Human:
"""A simple attempt to model a human."""
def __init__(self, name, age):
"""Initialize name and age attributes."""
self.name = name
self.age = age
def walk(self):
"""Simulate a human walking in response to a command."""
print(f"{self.name} is now walking.")
def run(self):
"""Simulate a human running in response to a command."""
print(f"{self.name} is now running!")5
A function defined inside a class is called a method. All rules governing functions apply to methods.
The __init__() method is a special initialization hook that Python executes automatically whenever a new instance is created.
The leading and trailing double underscores (dunder methods) indicate special internal language hooks.
__init__() initializes instance-specific attributes before the caller receives the newly allocated object.
Lifecycle flowchart:
my_human = Human('Willie', 6)Human.__init__(instance, 'Willie', 6).self.name and self.age are bound to the instance dictionary.my_human.class Human:
def __init__(self, name, age):
self.name = name
self.age = age
my_human = Human('Willie', 6)
print(f"Name: {my_human.name}, Age: {my_human.age}")6
Step-by-step flow of how Python creates an instance from a class.
Python reads the expression Human('Willie', 6).
Python creates a new, empty Human object on the heap.
Python passes the new object as the self parameter, together with 'Willie' and 6: Human.__init__(new_object, 'Willie', 6)
Inside the method, self.name = 'Willie' and self.age = 6 are stored in the object's attribute dictionary (__dict__).
The fully initialized object (name and age set) is assigned to the variable my_human for use.u
7
The self parameter is mandatory in method definitions and must be listed first in the parameter list.
When a method is called on an instance, Python passes the instance reference automatically as the first argument.
self provides explicit access to the specific instance's attributes and methods from within the class definition.
Mechanics breakdown:
In def __init__(self, name, age):
self: points to the newly allocated instance object.name and age: external arguments passed during invocation.In self.name = name:
name and binds it to the instance variable named name.self is accessible to every method in the class and to external callers holding the instance.8
Instantiation = creating an object from a class blueprint.
class Human is the template.
Human("An", 20) → self, name, age.
person1, person2 are separate objects.
Omit self when calling.
self = the object calling the method.
class Human:
def __init__(self, name, age):
self.name = name # Attach the name attribute to the current object (self)
self.age = age # Attach the age attribute to the current object (self)
def introduce(self):
# Thanks to self, the method knows whose name and age to use
print(f"Hello, I am {self.name}, {self.age} years old.")
person1 = Human("An", 20)
person2 = Human("Bình", 22)
person1.introduce() # Python implicitly runs: Human.introduce(person1) -> self is person1
person2.introduce() # Python implicitly runs: Human.introduce(person2) -> self is person2Hello, I am An, 20 years old.
Hello, I am Bình, 22 years old.9
Python utilizes dot notation (instance.attribute) to access the attributes of an object.
Dot notation instructs Python to look up the attribute name in the instance's private dictionary (__dict__).
If the attribute exists on the instance, its value is resolved; otherwise, Python checks the class attributes or raises an AttributeError.
class Human:
def __init__(self, name, age):
self.name = name
self.age = age
my_human = Human('Willie', 6)
# Accessing attributes via dot notation:
retrieved_name = my_human.name
retrieved_age = my_human.age
print(f"Retrieved: name={retrieved_name}, age={retrieved_age}")
print(f"Instance attribute dictionary: {my_human.__dict__}")Retrieved: name=Willie, age=6
Instance attribute dictionary: {'name': 'Willie', 'age': 6}10
After creating an instance, any method defined in the class can be invoked using dot notation: instance.method().
When calling my_human.walk(), Python internally transforms the call into Human.walk(my_human).
The caller does not supply an argument for self; Python supplies it implicitly.
class Human:
def __init__(self, name, age):
self.name = name
self.age = age
def walk(self):
print(f"{self.name} is now walking.")
def run(self):
print(f"{self.name} is now running!")
my_human = Human('Willie', 6)
my_human.walk()
my_human.run()Willie is now walking.
Willie is now running!11
A class serves as a reusable template. A program can instantiate as many distinct objects from a single class as required.
Each instance occupies its own distinct heap memory location and maintains independent attribute values.
Modifying or accessing attributes on one instance has zero side effects on other instances of the same class.
class Human:
def __init__(self, name, age):
self.name = name
self.age = age
def walk(self):
print(f"{self.name} is now walking.")
my_human = Human('Willie', 6)
your_human = Human('Lucy', 3)
print(f"My human is {my_human.name}, aged {my_human.age}.")
my_human.walk()
print(f"Your human is {your_human.name}, aged {your_human.age}.")
your_human.walk()My human is Willie, aged 6.
Willie is now walking.
Your human is Lucy, aged 3.
Lucy is now walking.12
Real-world software requires modeling complex domain entities with multiple attributes and calculated properties.
The Car class encapsulates the vehicle's manufacturing identity (make, model, year) and formats descriptive strings.
Methods can compute and return derived values by combining multiple instance attributes.
class Car:
"""A simple attempt to represent a car."""
def __init__(self, make, model, year):
"""Initialize attributes to describe a car."""
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
"""Return a neatly formatted descriptive name."""
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())2019 Audi A4Sources: Coursebook Section 8.2.1 / Eric Matthes, Python Crash Course (2nd ed.)
13
Not every attribute needs to be populated via an explicit parameter in __init__().
When an attribute represents an initial state that is constant for all newly minted objects, it can be defined directly inside __init__().
In this pattern, the parameter list remains concise while the object instance gains a fully initialized attribute.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}".title()
def read_odometer(self):
"""Print a statement showing the car's mileage."""
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()2019 Audi A4
This car has 0 miles on it.14
In Python, attributes on an instance are by default public and mutable.
A caller can modify an attribute directly by assigning a new value via dot notation: instance.attribute = value.
While simple and concise, direct attribute modification bypasses validation logic, allowing illegal states.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car = Car('audi', 'a4', 2019)
# Direct attribute modification:
my_new_car.odometer_reading = 23
my_new_car.read_odometer()This car has 23 miles on it.15
Encapsulation is an OOP principle where internal state is modified through designated method interfaces rather than direct assignment.
Setter methods take incoming data as parameters and update internal attributes on the caller's behalf.
Provides a centralized location where updates occur, improving maintainability, auditability, and debugging.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def update_odometer(self, mileage):
"""Set the odometer reading to the given value."""
self.odometer_readin g = mileage
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car = Car('audi', 'a4', 2019)
my_new_car.update_odometer(23)
my_new_car.read_odometer()This car has 23 miles on it.Sources: Coursebook Section 8.2.3
16
Passing values through methods enables defensive programming: validating inputs before altering object state.
Invariants are logical assertions that must always remain true for an object to be in a valid operational state.
For an odometer, an invariant requires that mileage can never decrease (no odometer rollbacks).
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 20
def update_odometer(self, mileage):
"""Set the odometer reading. Reject rollbacks."""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("Error: You can't roll back an odometer!")
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")
my_car = Car('audi', 'a4', 2019)
my_car.update_odometer(50)
my_car.read_odometer()
# Attempting an illegal rollback:
my_car.update_odometer(30)
my_car.read_odometer()This car has 50 miles on it.
Error: You can't roll back an odometer!
This car has 50 miles on it.17
In many operational contexts, an attribute represents an accumulator (e.g., packet counters, network hops, mileage).
Rather than calculating the absolute total outside the class, a dedicated method accepts an incremental delta.
The method adds the delta to the existing attribute value using the augmented assignment operator (+=).
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def update_odometer(self, mileage):
if mileage >= self.odometer_reading
self.odometer_reading = mileage
def increment_odometer(self, miles):
"""Add the given amount to the odometer reading."""
if miles > 0:
self.odometer_reading += miles
else:
print("Warning: Only positive mileage increments are permitted.")
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")
my_used_car = Car('subaru', 'outback', 2015)
my_used_car.update_odometer(23500)
my_used_car.read_odometer()
my_used_car.increment_odometer(100)
my_used_car.read_odometer()This car has 23500 miles on it.
This car has 23600 miles on it.18
Unlike Java or C++, Python does not enforce private variables at the bytecode compiler level.
Instead, the language community relies on strict naming conventions and name mangling mechanisms:
Intended for direct access by any caller.
Single leading underscore signals internal use. Linters and IDEs warn callers against direct external access, though Python does not block it.
Double leading underscores trigger name mangling, where the interpreter rewrites the identifier as _ClassName__secret to prevent accidental namespace collisions in subclasses.
19
class SecurityVault:
def __init__(self, secret_key):
self.public_id = "VAULT-01"
self._internal_state = "active"
self.__secret_key = secret_key
vault = SecurityVault("RSA_PRIV_KEY_9941")
print("Public ID:", vault.public_id)
print("Protected state:", vault._internal_state)
# Direct access to mangled attribute raises AttributeError:
try:
print(vault.__secret_key)
except AttributeError as e:
print("Caught expected error:", e)
print("Access via mangled name:", vault._SecurityVault__secret_key)Public ID: VAULT-01
Protected state: active
Caught expected error: 'SecurityVault' object has no attribute '__secret_key'
Access via mangled name: RSA_PRIV_KEY_994120
Applying class-based state encapsulation to Information Security: modeling user authentication states and brute-force defenses.
An authentication class encapsulates login attempt counters, lockout thresholds, and account states.
Method logic increments counters upon failed authentications and locks the account when thresholds are breached.
class UserAccount:
"""Manages user identity and authentication defenses."""
def __init__(self, username: str, max_attempts: int = 3):
self.username = username
self.login_attempts = 0
self.max_attempts = max_attempts
self.is_locked = False
def record_failed_attempt(self):
"""Increment failure counter and enforce lockout when threshold reached."""
self.login_attempts += 1
print(f"[{self.username}] Failed attempt logged. Total: {self.login_attempts}")
if self.login_attempts >= self.max_attempts:
self.is_locked = True
print(f"ALERT: Account {self.username} is LOCKED due to excessive failed attempts.")
def reset_login_attempts(self):
"""Reset counter upon verified authentication."""
self.login_attempts = 0
print(f"[{self.username}] Login attempts reset to 0.")
user = UserAccount("analyst_sec")
user.record_failed_attempt()
user.record_failed_attempt()
user.record_failed_attempt()
print("Account status locked?:", user.is_locked)[analyst_sec] Failed attempt logged. Total: 1
[analyst_sec] Failed attempt logged. Total: 2
[analyst_sec] Failed attempt logged. Total: 3
ALERT: Account analyst_sec is LOCKED due to excessive failed attempts.
Account status locked?: True21
Inheritance enables a new class (subclass or child class) to inherit attributes and methods from an existing class (superclass or parent class).
Eliminates redundant code by centralizing shared logic in base classes while specializing unique logic in derived classes.
Represents an "is-a" architectural relationship (e.g., an ElectricCar is a Car; a FirewallRule is a NetworkPolicy).
[ Base Class: Car ]
(make, model, year, odometer)
│
┌───────┴───────┐
▼ ▼
[ ElectricCar ] [ GasolineCar ]
(battery, (fuel_tank,
charging) emissions)22
When defining a subclass, the class declaration specifies the parent class inside parentheses: class ElectricCar(Car):.
The parent class must be defined in the current scope or imported before the subclass definition.
The subclass constructor must invoke the parent class constructor to initialize inherited attributes.
super() is a built-in helper function that returns a proxy object delegating method calls to the parent class.
class Car:
"""A simple attempt to represent a car."""
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}".title()
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
"""Initialize attributes of the parent class."""
super().__init__(make, model, year)
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())2019 Tesla Model S23
Once constructor chaining is established via super(), the child class inherits all methods from the parent class without manual reimplementation.
The child class instance possesses all attributes defined in the parent class constructor.
Any method defined on Car (e.g., read_odometer(), update_odometer()) can be invoked directly on an ElectricCar instance.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def update_odometer(self, mileage):
self.odometer_reading = mileage
def read_odometer(self):
print(f"Mileage: {self.odometer_reading} miles.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
my_ev = ElectricCar('nissan', 'leaf', 2021)
my_ev.update_odometer(12_400)
my_ev.read_odometer()Mileage: 12400 miles.Sources: Coursebook Section 8.3.1
24
A subclass differentiates itself from its parent by adding new attributes and methods that apply exclusively to its specialized domain.
In ElectricCar, traditional attributes (make, model, year) are passed upward to Car, while specialized attributes (battery_size) are initialized locally.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}".title()
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self.battery_size = 75
def describe_battery(self):
"""Print a statement describing the battery size."""
print(f"This car has a {self.battery_size}-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()2019 Tesla Model S
This car has a 75-kWh battery.Sources: Coursebook Section 8.3.2
25
When a parent method does not match the requirements or real-world behavior of a subclass, the subclass can override that method.
To override a method, define a method in the subclass with the exact same name as the method in the parent class.
When the method is called on a child instance, Python executes the child's implementation and ignores the parent's definition.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def fill_gas_tank(self):
"""Gas cars require liquid fuel refills."""
print("Fuel tank filled with gasoline.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
def fill_gas_tank(self):
"""Electric cars do not have gas tanks."""
print("This car doesn't need a gas tank! Plug into a charging station.")
gas_car = Car('ford', 'mustang', 2020)
ev_car = ElectricCar('tesla', 'model 3', 2022)
gas_car.fill_gas_tank()
ev_car.fill_gas_tank()Fuel tank filled with gasoline.
This car doesn't need a gas tank! Plug into a charging station.Sources: Coursebook Section 8.3.3
26
As software evolves, classes risk growing bloated if every detail is added directly into a single class.
Composition is a design principle where a class contains references to instances of other classes as attributes.
Represents a "has-a" relationship (e.g., an ElectricCar has a Battery; a Firewall has a RuleSet).
Keeps individual classes small, modular, and focused on a single responsibility (SRP).
class Battery:
"""A simple attempt to model a battery for an electric car."""
def __init__(self, battery_size=75):
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
# Storing an instance of Battery as an attribute:
self.battery = Battery()
my_tesla = ElectricCar('tesla', 'model s', 2019)
my_tesla.battery.describe_battery()This car has a 75-kWh battery.Sources: Coursebook Section 8.3.4
27
With composition, complex logic related to a sub-component can be added to the sub-component's class without modifying the enclosing parent class.
The Battery class can grow to calculate driving range, battery health degradation, and charging curves independently.
class Battery:
def __init__(self, battery_size=75):
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def get_range(self):
"""Print estimated range based on battery capacity."""
if self.battery_size == 75:
travel_range = 260
elif self.battery_size == 100:
travel_range = 315
else:
travel_range = int(self.battery_size * 3.4)
print(f"This car can go about {travel_range} miles on a full charge.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self.battery = Battery(75)
my_tesla = ElectricCar('tesla', 'model s', 2019)
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()This car has a 75-kWh battery.
This car can go about 260 miles on a full charge.Sources: Coursebook Section 8.3.4
28
A foundational object-oriented design principle states: "Favor object composition over class inheritance" (Gang of Four design patterns).
Inheritance couples subclasses tightly to superclass implementation details, creating rigid hierarchies.
Composition offers loose coupling: component classes can be swapped, tested in isolation, or reused across unrelated classes.
29
Python supports multiple inheritance, allowing a class to inherit attributes and methods from more than one parent class: class SubClass(BaseA, BaseB):.
When resolving attributes and methods, Python must prevent ambiguity (the diamond problem).
Python resolves method dispatch using the C3 Linearization algorithm to compute a deterministic Method Resolution Order (MRO).
class Loggable:
def audit(self, event):
print(f"[AUDIT LOG] {event}")
class Authenticable:
def verify_token(self, token):
return token == "valid_bearer_token"
class AdminSession(Loggable, Authenticable):
def __init__(self, user):
self.user = user
session = AdminSession("sec_officer")
session.audit("Login initiated")
print("Token valid:", session.verify_token("valid_bearer_token"))
print("MRO Resolution Order:")
for cls in AdminSession.mro():
print(" ->", cls.__name__)[AUDIT LOG] Login initiated
Token valid: True
MRO Resolution Order:
-> AdminSession
-> Loggable
-> Authenticable
-> object30
Applying inheritance and composition to InfoSec: implementing Role-Based Access Control (RBAC).
Base User encapsulates identity and general authentication.
Admin inherits from User and composes a Privileges instance, ensuring privilege logic is isolated and cleanly audited.
class User:
def __init__(self, first_name: str, last_name: str, username: str):
self.first_name = first_name
self.last_name = last_name
self.username = username
def describe_user(self):
print(f"User: {self.first_name} {self.last_name} (@{self.username})")
class Privileges:
"""Manages authorization rules for privileged accounts."""
def __init__(self, privileges=None):
self.privileges = privileges if privileges is not None else []
def show_privileges(self):
print("Assigned Security Privileges:")
for priv in self.privileges:
print(f" [+] {priv}")
class Admin(User):
"""Specialized administrator user with composed privileges."""
def __init__(self, first_name: str, last_name: str, username: str):
super().__init__(first_name, last_name, username)
self.privileges = Privileges([
"rule:allow_ingress_port",
"audit:read_raw_pcap",
"policy:enforce_lockout"
])
admin_user = Admin("Duy", "Tran", "duy_admin")
admin_user.describe_user()
admin_user.privileges.show_privileges()User: Duy Tran (@duy_admin)
Assigned Security Privileges:
[+] rule:allow_ingress_port
[+] audit:read_raw_pcap
[+] policy:enforce_lockoutSources: Coursebook Exercises 8-7 & 8-8 (Admin & Privileges) / NIST SP 800-162 (RBAC Architecture)
31
As classes expand with realistic methods and attributes, keeping all definitions in a single program file makes maintenance difficult.
Python allows classes to be stored in external module files (.py) and imported into executable scripts as needed.
This promotes separation of concerns: module files store structural class definitions, while main program files drive operational workflows.
project_root/
│
├── car.py # Class definitions module (Car, Battery, ElectricCar)
├── my_cars.py # Main driver program
└── security_tools/ # Subpackage
├── __init__.py
└── firewall.py32
To import a specific class from a module, Python uses the syntax: from module_name import ClassName.
The import statement searches the module file, loads its compiled code, and introduces the specific class identifier into the current namespace.
class Car:
"""A class that can be used to represent a car."""
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 100
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}".title()from car import Car
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
print(f"Current odometer: {my_new_car.odometer_reading}")2019 Audi A4
Current odometer: 23Sources: Coursebook Section 8.4.1
33
A single module file can store as many classes as necessary, provided each class is logically cohesive and shares a common domain purpose.
Storing Car, Battery, and ElectricCar together in car.py keeps automotive abstractions organized in one place.
The module can define standalone helper classes as well as inheritance hierarchies.
"""Module car: Provides models for combustion and electric vehicles."""
class Car:
... # Base vehicle attributes and odometer controls
class Battery:
... # Component modeling battery capacity and range
class ElectricCar(Car):
... # Subclass inheriting from Car and composing BatteryCohesion principle: Classes placed in the same module should be modified together for similar reasons. Unrelated classes belong in separate modules.
34
When a driver script requires multiple classes from the same module, list each class separated by a comma.
Syntax: from module_name import ClassA, ClassB.
Only the specified identifiers enter the local namespace, keeping it clean and collision-free.
# Simulating execution from car.py:
from car import Car, ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())2019 Volkswagen Beetle
2019 Tesla RoadsterEngineering benefit: Explicit imports make dependencies immediately visible at the top of the file without polluting the global namespace.
Sources: Coursebook Section 8.4.3
35
An entire module can be imported using the syntax: import module_name.
When imported this way, individual classes are accessed via dot notation: module_name.ClassName.
Prevents naming collisions when two external modules define classes with identical names (e.g., config.User vs. db.User).
# Driver program importing module namespace:
import car
my_beetle = car.Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
my_tesla = car.ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())2019 Volkswagen Beetle
2019 Tesla RoadsterGlobal Scope: [ car (module object) ]
│
├── car.Car
├── car.Battery
└── car.ElectricCarSources: Coursebook Section 8.4.4
36
In production architectures, one module may import classes from another module before exporting its own specialized classes.
For example, electric_car.py can import Car from car.py.
Wildcard imports (from module import *) must be avoided in production code.
from car import *from car import Car, ElectricCarimport car37
When a class name is lengthy, conflicts with an existing identifier, or needs domain clarification, Python allows aliasing using the as keyword.
Aliasing can be applied to individual imported classes or to entire module namespaces.
from car import ElectricCar as EC
import statistics as stats
# Instantiating through the imported alias:
fleet_vehicle = EC('tesla', 'model 3', 2023)
print(f"Aliased object instance: {fleet_vehicle.get_descriptive_name()}")
# Invoking module functions through alias:
latencies = [12, 15, 11, 14, 18]
print(f"Average network latency: {stats.mean(latencies)} ms")Aliased object instance: 2023 Tesla Model 3
Average network latency: 14 msBest practices note: Use standard, community-accepted aliases (e.g., import numpy as np, import pandas as pd).
38
The Python Standard Library is a comprehensive suite of built-in modules bundled with every CPython distribution.
The random module provides pseudo-random number generation (PRNG) algorithms based on the Mersenne Twister engine.
Returns a random integer N such that a <= N <= b.
Selects a random element from a non-empty sequence.
Returns a unique k-length selection without replacement.
Shuffles a mutable sequence in place.
import random
random.seed(42) # Fixed seed for reproducibility
print("randint(1, 100):", random.randint(1, 100))
hosts = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"]
print("Random target host:", random.choice(hosts))
print("Sample 2 targets:", random.sample(hosts, 2))randint(1, 100): 82
Random target host: 10.0.0.1
Sample 2 targets: ['10.0.0.1', '10.0.0.3']39
A critical Information Security distinction: Standard PRNGs are NOT cryptographically secure.
The random module uses Mersenne Twister (MT19937), whose internal state (624 32-bit words) can be fully reconstructed after observing 624 outputs.
Never use random for cryptographic keys, authentication nonces, initialization vectors (IVs), or password reset tokens.
For security-sensitive randomness, Python provides the secrets module, which wraps OS-level entropy pools (/dev/urandom).
import secrets
token = secrets.token_hex(16)
print("Cryptographically secure nonce:", token)Cryptographically secure nonce: e2c94a86f2b0513982cd67a14e9f5e1b40
Python's built-in statistics module calculates mathematical statistics on real-valued numeric data.
Essential for computing baseline metrics in cybersecurity monitoring (e.g., detecting anomaly spikes in packet rates).
Calculates central tendencies (mean, median, mode) and dispersion (variance, standard deviation).
import statistics
import random
random.seed(10)
data = random.sample(range(10, 101), 10)
print("Sampled telemetry points:", data)
print("Mean: ", statistics.mean(data))
print("Median: ", statistics.median(data))
print("Sample StDev: ", round(statistics.stdev(data), 2))
print("Sample Variance: ", round(statistics.variance(data), 2))Sampled telemetry points: [83, 14, 64, 71, 11, 36, 69, 72, 45, 93]
Mean: 55.8
Median: 66.5
Sample StDev: 28.13
Sample Variance: 791.2941
NumPy (Numerical Python) is the foundation of the scientific Python ecosystem.
Python lists store pointers to scattered objects with dynamic type overhead.
NumPy provides the ndarray (n-dimensional array): a contiguous block of homogeneous memory executed via compiled C routines.
Enables vectorization: applying arithmetic and logical operations across entire arrays simultaneously without explicit Python for loops.
Python List:
[ ptr ] ───> [ PyObject: int 10 ]
[ ptr ] ───> [ PyObject: int 20 ]
(Fragmented heap)
NumPy Array:
[ int32: 10 ][ int32: 20 ][ int32: 30 ]
(Contiguous C buffer)import numpy as np
arr = np.array([10, 25, 40, 55, 70])
print("NumPy array:", arr)
print("Shape:", arr.shape)
print("Mean via vectorization:", arr.mean())NumPy array: [10 25 40 55 70]
Shape: (5,)
Mean via vectorization: 40.0Sources: https://numpy.org/doc/stable/
42
Security applications frequently process multi-dimensional data, such as traffic matrices, pixel grids in steganography, or feature vectors.
NumPy handles 2D matrices effortlessly, enabling fast boolean indexing and threshold filtering.
import numpy as np
# Simulating packet count matrix across 4 subnets over 3 time windows:
traffic_matrix = np.array([
[120, 140, 110],
[950, 980, 1020], # Suspicious high-volume subnet
[ 80, 95, 85],
[210, 205, 230]
])
print("Traffic Matrix:\n", traffic_matrix)
print("Total packets per subnet:", traffic_matrix.sum(axis=1))
# Vectorized condition: find subnets exceeding 900 packets average:
subnet_means = traffic_matrix.mean(axis=1)
suspicious = subnet_means > 500
print("Suspicious subnet mask:", suspicious)Traffic Matrix:
[[ 120 140 110]
[ 950 980 1020]
[ 80 95 85]
[ 210 205 230]]
Total packets per subnet: [ 370 2950 260 645]
Suspicious subnet mask: [False True False False]43
Pandas is an open-source data manipulation and analysis library built on top of NumPy.
Provides two primary data structures:
Ideal for ingesting, querying, filtering, and cleaning forensic artifacts, SIEM exports, and authentication logs.
import pandas as pd
# Initializing a Pandas DataFrame from the coursebook syllabus:
df = pd.DataFrame({
'X': [78, 85, 96, 80, 86],
'Y': [84, 94, 89, 83, 86],
'Z': [86, 97, 96, 72, 83]
})
print("DataFrame Inspection:")
print(df)DataFrame Inspection:
X Y Z
0 78 84 86
1 85 94 97
2 96 89 96
3 80 83 72
4 86 86 83Sources: https://pandas.pydata.org/docs/
44
DataFrames enable concise querying, boolean indexing, column projection, and aggregations on security data.
Common operations include checking for null records, filtering open network ports, and summarizing attack categories.
import pandas as pd
# Creating a port audit dataset:
audit_data = pd.DataFrame({
'port': [22, 80, 443, 8080],
'service': ['SSH', 'HTTP', 'HTTPS', 'HTTP-Proxy'],
'state': ['open', 'open', 'open', 'filtered']
})
print("Raw Audit Table:\n", audit_data)
# Filtering open services:
open_services = audit_data[audit_data['state'] == 'open']
print("\nOpen Services Only:\n", open_services[['port', 'service']])Raw Audit Table:
port service state
0 22 SSH open
1 80 HTTP open
2 443 HTTPS open
3 8080 HTTP-Proxy filtered
Open Services Only:
port service
0 22 SSH
1 80 HTTP
2 443 HTTPS45
Python's standard library includes hundreds of specialized modules (the "batteries included" philosophy).
PyMOTW (Python Module of the Week) is an authoritative, community-standard reference created by Doug Hellmann demonstrating standard library usage with clean, executable examples.
Cryptographic digests and message authentication.
Raw network communication and TLS transport encryption.
IP routing, subnet calculation, and CIDR parsing.
Executing system binaries safely without shell injection.
Filesystem and runtime environment inspection.
46
Professional object-oriented Python conforms to PEP 8 standards and PEP 484 type annotations.
Use CapWords (PascalCase), e.g., NetworkDevice, SecurityScanner.
Every class must feature a top-level docstring describing its responsibility. Methods follow function docstring rules.
Surround top-level class definitions with two blank lines. Inside a class, separate methods with a single blank line.
Annotate constructor arguments and method return signatures.
class FirewallRule:
"""Represents an atomic packet filtering rule."""
def __init__(self, port: int, protocol: str, action: str = "DENY") -> None:
self.port: int = port
self.protocol: str = protocol.upper()
self.action: str = action.upper()
def evaluate(self, target_port: int, target_proto: str) -> bool:
"""Evaluate if an incoming packet matches this rule."""
return self.port == target_port and self.protocol == target_proto.upper()
rule = FirewallRule(443, "tcp", "ALLOW")
print(f"Rule configured: {rule.protocol}/{rule.port} -> {rule.action}")
print("Packet match (443/TCP):", rule.evaluate(443, "tcp"))
print("Packet match (80/TCP): ", rule.evaluate(80, "tcp"))Rule configured: TCP/443 -> ALLOW
Packet match (443/TCP): True
Packet match (80/TCP): False47
Structured hands-on lab problems from Coursebook Chapter 8 reinforcing OOP mastery:
describe_restaurant() across multiple instances.first_name, last_name, and greet_user().set_number_served() and increment_number_served().increment_login_attempts() and reset_login_attempts().Restaurant and User.Admin by creating a standalone Privileges class as a composed attribute.upgrade_battery() and verify updated driving range.random.randint() and random.choice().Sources: Coursebook Section "Bài tập chương 8" (Exercises 8-1 to 8-16)
48
A synthesized lab demonstration integrating encapsulation, inheritance, composition, and defensive validation.
Implements a Packet data model, a composite RuleSet, and an extensible Firewall class.
class Packet:
def __init__(self, src_ip: str, dst_port: int, protocol: str):
self.src_ip = src_ip
self.dst_port = dst_port
self.protocol = protocol.upper()
class RuleSet:
def __init__(self):
self.blocked_ports = {21, 23, 445} # Block FTP, Telnet, SMB
def is_port_blocked(self, port: int) -> bool:
return port in self.blocked_ports
class Firewall:
def __init__(self, name: str):
self.name = name
self.rules = RuleSet() # Composition
self.inspected_count = 0 # Internal state
def inspect(self, pkt: Packet) -> str:
self.inspected_count += 1
if self.rules.is_port_blocked(pkt.dst_port):
return f"[{self.name}] DROP packet to port {pkt.dst_port} from {pkt.src_ip}"
return f"[{self.name}] ALLOW packet to port {pkt.dst_port} from {pkt.src_ip}"
fw = Firewall("Perimeter-FW")
p1 = Packet("192.168.1.10", 443, "tcp")
p2 = Packet("10.0.0.99", 23, "tcp")
print(fw.inspect(p1))
print(fw.inspect(p2))
print("Total inspected packets:", fw.inspected_count)[Perimeter-FW] ALLOW packet to port 443 from 192.168.1.10
[Perimeter-FW] DROP packet to port 23 from 10.0.0.99
Total inspected packets: 2Sources: Coursebook Chapter 8 OOP Synthesis / Python for Cybersecurity (Wiley, 2022)
49
Summary of core paradigms mastered in Session 5:
Bundling state (attributes) and behavior (methods) into classes, instantiated via __init__(self).
Modifying attributes via accessor/mutator methods to enforce invariants and prevent illegal transitions.
Leveraging single inheritance with super() and polymorphic method overriding.
Decomposing complex software into interacting components using instances as attributes.
Distributing classes across modules and applying standard/scientific libraries (random, statistics, NumPy, Pandas).
with open(...) as f:).try-except-else-finally blocks.50

51
#include <iostream>
#include <string>
using namespace std;
class Vault {
private:
string secretKey;
protected:
int accessLevel;
public:
string vaultId;
Vault(string id, string key) : secretKey(key), accessLevel(3), vaultId(id) {}
friend void auditVault(const Vault& v);
};
void auditVault(const Vault& v) {
cout << "[Audit] " << v.vaultId << " key length = " << v.secretKey.size() << endl;
}
int main() {
Vault v("VAULT-01", "RSA_PRIV_KEY_9941");
cout << "Public ID: " << v.vaultId << endl;
// cout << v.secretKey; // compile error: 'secretKey' is a private member
auditVault(v);
return 0;
}Output:
Public ID: VAULT-01
[Audit] VAULT-01 key length = 17Neither language access model is a security boundary against an attacker with code execution in the same process. C++ private data can be read through raw memory; Python private data through _ClassName__name. Secrets need proper key management, not access specifiers.
52
#include <iostream>
#include <string>
using namespace std;
class Session {
string user;
public:
Session(string u) : user(u) { cout << "Open session: " << user << endl; }
~Session() { cout << "Close session: " << user << endl; }
};
int main() {
cout << "Enter scope" << endl;
{
Session s("analyst");
cout << "Working..." << endl;
}
cout << "Left scope" << endl;
}class Session:
def __init__(self, user):
self.user = user
print(f"Open session: {self.user}")
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
print(f"Close session: {self.user}")
return False
print("Enter scope")
with Session("analyst") as s:
print("Working...")
print("Left scope")Enter scope
Open session: analyst
Working...
Close session: analyst
Left scope53
class Host {
public:
string ip;
Host(string addr = "0.0.0.0") : ip(addr) {}
void ping() const { cout << "Ping " << ip << endl; }
};
int main() {
Host* h1 = new Host("10.0.0.1"); // manual heap allocation
h1->ping();
delete h1; // must free manually
h1 = nullptr;
auto h2 = make_unique<Host>("10.0.0.2"); // RAII smart pointer
h2->ping();
Host fleet[3] = { Host("10.0.0.3"), Host("10.0.0.4"), Host("10.0.0.5") };
for (const Host& h : fleet) h.ping();
}Output:
Ping 10.0.0.1
Ping 10.0.0.2
Ping 10.0.0.3
Ping 10.0.0.4
Ping 10.0.0.5class Host:
def __init__(self, ip="0.0.0.0"):
self.ip = ip
def ping(self):
print(f"Ping {self.ip}")
h1 = Host("10.0.0.1")
h2 = h1 # copies the reference, not the object
h2.ip = "10.0.0.99"
h1.ping()
print("Same object:", h1 is h2)
fleet = [Host(f"10.0.0.{i}") for i in range(3, 6)]
for h in fleet: h.ping()Output:
Ping 10.0.0.99
Same object: True
Ping 10.0.0.3
Ping 10.0.0.4
Ping 10.0.0.554
class Device {
public: void powerOn() { cout << "Power on" << endl; }
protected: void flash() { cout << "Flash firmware" << endl; }
};
class Router : public Device { // public inheritance: is-a
public:
void upgrade() { flash(); } // protected member accessible here
};
class Appliance : private Device { // private inheritance
public:
void start() { powerOn(); }
};
int main() {
Router r;
r.powerOn();
r.upgrade();
Appliance a;
a.start();
// a.powerOn(); // error: powerOn is inaccessible via private inheritance
}Output:
Power on
Flash firmware
Power onclass Device:
def power_on(self): print("Power on")
def _flash(self): print("Flash firmware")
class Router(Device): # the only inheritance mode
def upgrade(self): self._flash()Derivation access table (C++ base member → visibility in derived)
55
class Car {
protected:
string make;
public:
Car(string m) : make(m) { cout << "Car() constructor" << endl; }
~Car() { cout << "~Car() destructor" << endl; }
};
class ElectricCar : public Car {
int batterySize;
public:
ElectricCar(string m, int b) : Car(m), batterySize(b) {
cout << "ElectricCar() constructor, battery = " << batterySize << endl;
}
~ElectricCar() { cout << "~ElectricCar() destructor" << endl; }
};
int main() {
ElectricCar ev("tesla", 75);
cout << "--- end of main ---" << endl;
}Output:
Car() constructor
ElectricCar() constructor, battery = 75
--- end of main ---
~ElectricCar() destructor
~Car() destructorclass Car:
def __init__(self, make):
self.make = make
print("Car.__init__")
class ElectricCar(Car):
def __init__(self, make, battery_size):
print("ElectricCar.__init__ starts")
super().__init__(make)
self.battery_size = battery_size
print(f"ElectricCar.__init__ ends, battery = {self.battery_size}")
class Lazy(Car):
def __init__(self, make):
pass # super().__init__ omitted
ev = ElectricCar("tesla", 75)
try:
print(Lazy("x").make)
except AttributeError as e:
print("AttributeError:", e)Output:
ElectricCar.__init__ starts
Car.__init__
ElectricCar.__init__ ends, battery = 75
AttributeError: 'Lazy' object has no attribute 'make'56
class Packet {
public:
string src = "10.0.0.9";
void describe() const { cout << "Packet from " << src << endl; }
};
class TcpPacket : public Packet {
public:
int port = 443;
void describe() const { cout << "TCP packet from " << src << " port " << port << endl; }
};
int main() {
TcpPacket t;
Packet p = t; // object slicing: port is cut off
Packet* ptr = &t; // upcast pointer, non-virtual call
t.describe();
p.describe();
ptr->describe();
}Output:
TCP packet from 10.0.0.9 port 443
Packet from 10.0.0.9
Packet from 10.0.0.9class Packet:
src = "10.0.0.9"
def describe(self):
print(f"Packet from {self.src}")
class TcpPacket(Packet):
port = 443
def describe(self):
print(f"TCP packet from {self.src} port {self.port}")
t = TcpPacket()
p: Packet = t # type hint only; same object
p.describe()
print(isinstance(p, Packet), type(p).__name__)Output:
TCP packet from 10.0.0.9 port 443
True TcpPacket57
class Scanner {
public:
virtual void scan() const { cout << "Generic scan" << endl; }
virtual ~Scanner() = default;
};
class PortScanner : public Scanner {
public:
void scan() const override { cout << "TCP port scan" << endl; }
};
class WebScanner : public Scanner {
public:
void scan() const override { cout << "HTTP header scan" << endl; }
};
int main() {
vector<unique_ptr<Scanner>> tools;
tools.push_back(make_unique<Scanner>());
tools.push_back(make_unique<PortScanner>());
tools.push_back(make_unique<WebScanner>());
for (const auto& t : tools) t->scan(); // dynamic binding via vtable
}Output:
Generic scan
TCP port scan
HTTP header scanclass PortScanner:
def scan(self): print("TCP port scan")
class WebScanner:
def scan(self): print("HTTP header scan")
for tool in [PortScanner(), WebScanner()]:
tool.scan()Output:
TCP port scan
HTTP header scan58
class Detector { // abstract base class
public:
virtual bool inspect(int port) const = 0; // pure virtual
virtual ~Detector() = default;
};
class TelnetDetector : public Detector {
public:
bool inspect(int port) const override { return port == 23; }
};
int main() {
// Detector d; // error: cannot instantiate abstract class
unique_ptr<Detector> d = make_unique<TelnetDetector>();
cout << boolalpha;
cout << "Port 23 flagged: " << d->inspect(23) << endl;
cout << "Port 443 flagged: " << d->inspect(443) << endl;
}Output:
Port 23 flagged: true
Port 443 flagged: falsefrom abc import ABC, abstractmethod
class Detector(ABC):
@abstractmethod
def inspect(self, port: int) -> bool: ...
class TelnetDetector(Detector):
def inspect(self, port: int) -> bool:
return port == 23
try:
Detector()
except TypeError as e:
print("TypeError:", e)
d = TelnetDetector()
print("Port 23 flagged:", d.inspect(23))
print("Port 443 flagged:", d.inspect(443))Output (CPython 3.9; wording of the TypeError message differs slightly in 3.12+):
TypeError: Can't instantiate abstract class Detector with abstract method inspect
Port 23 flagged: True
Port 443 flagged: False59
virtual public Device shares a single base subobject.super() calls follow the MRO computed by C3 linearization, so Device.__init__ runs exactly once.class Device {
public:
Device() { cout << "Device() constructed" << endl; }
void id() const { cout << "Device id" << endl; }
};
class Firewall : virtual public Device {
public: Firewall() { cout << "Firewall()" << endl; }
};
class Router : virtual public Device {
public: Router() { cout << "Router()" << endl; }
};
class UTMAppliance : public Firewall, public Router {
public: UTMAppliance() { cout << "UTMAppliance()" << endl; }
};
int main() {
UTMAppliance utm;
utm.id(); // unambiguous thanks to virtual inheritance
}Output:
Device() constructed
Firewall()
Router()
UTMAppliance()
Device idclass Device:
def __init__(self):
print("Device.__init__")
super().__init__()
class Firewall(Device):
def __init__(self):
print("Firewall.__init__")
super().__init__()
class Router(Device):
def __init__(self):
print("Router.__init__")
super().__init__()
class UTMAppliance(Firewall, Router):
def __init__(self):
print("UTMAppliance.__init__")
super().__init__()
UTMAppliance()
print([c.__name__ for c in UTMAppliance.__mro__])Output:
UTMAppliance.__init__
Firewall.__init__
Router.__init__
Device.__init__
['UTMAppliance', 'Firewall', 'Router', 'Device', 'object']Same OOP pillars, different enforcement points — C++ checks at compile time and gives control over memory; Python checks at runtime and gives flexibility. Security tooling often combines both: C/C++ for performance-critical engines, Python for orchestration and automation.
60
Object-Oriented Programming (Classes)