Object-Oriented Programming (Classes)

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")
True

1

Agenda

Column A — Creating Classes, Instances & State Management

8.1 Creating and Using a Class

  • Object-oriented paradigm: state, behavior, and instances
  • Creating class, the __init__() constructor, and self
  • Instantiating objects, accessing attributes, and invoking methods

8.2 Working with Classes and Instances

  • The Car class model and default attribute initialization
  • Modifying attributes directly vs. using accessor/setter methods
  • Enforcing invariant rules, input validation, and increment logic
  • Information hiding conventions (_single_leading_underscore, __mangling)

Column B — Inheritance, Modules & Standard Libraries

8.3 Inheritance & Object Composition

  • Parent class and subclass relationships (is-a vs. has-a)
  • Constructor chaining with super().__init__()
  • Overriding parent methods and polymorphic execution
  • Composition: instances as attributes (ElectricCar and Battery)

8.4 Storing and Importing Classes in Modules

  • Module organization, single vs. multiple class imports
  • Importing full modules, alias configuration, and namespace hygiene

8.5 The Python Standard Library & Scientific Computing

  • random module mechanisms vs. secrets for security
  • statistics module calculations, NumPy arrays, and Pandas DataFrames
  • Exercises 8-1 to 8-16 roadmap

2

Session Learning Outcomes

1

Model Real-World Systems with OOP

Define classes using the class keyword, __init__() constructor, self parameter, and encapsulate state and behavior into concrete objects.

2

Control Object State Safely

Implement default attribute values, manage attribute modification via guarded setter methods, and enforce data invariants defensively.

3

Design Reusable Hierarchies

Implement single inheritance with super(), override parent behaviors polymorphically, and decompose complex systems using object composition.

4

Modularize Software and Leverage Core Libraries

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

Foundations of Object-Oriented Programming

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

Defining a Class: The Human Model

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

The Special Constructor Method: __init__()

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:

  1. Expression evaluated: my_human = Human('Willie', 6)
  1. Python allocates a fresh heap memory block for the instance.
  1. Python dispatches Human.__init__(instance, 'Willie', 6).
  1. Attributes self.name and self.age are bound to the instance dictionary.
  1. Reference to the initialized instance is returned and assigned to 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

Object Creation Flow: What Happens When You Run Human('Willie', 6)

Step-by-step flow of how Python creates an instance from a class.

01

Call the class

Python reads the expression Human('Willie', 6).

02

Allocate memory

Python creates a new, empty Human object on the heap.

03

Auto-call __init__

Python passes the new object as the self parameter, together with 'Willie' and 6: Human.__init__(new_object, 'Willie', 6)

04

Assign attributes

Inside the method, self.name = 'Willie' and self.age = 6 are stored in the object's attribute dictionary (__dict__).

05

Return the result

The fully initialized object (name and age set) is assigned to the variable my_human for use.u

7

The Role of the self Parameter

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:

  • Takes the value associated with parameter name and binds it to the instance variable named name.
  • Any variable prefixed with self is accessible to every method in the class and to external callers holding the instance.

8

Instantiating an Object from a Class

Instantiation = creating an object from a class blueprint.

Blueprint

class Human is the template.

Call

Human("An", 20) → self, name, age.

Instances

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 person2

Output

Hello, I am An, 20 years old.
Hello, I am Bình, 22 years old.

9

Accessing Instance Attributes

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__}")

Output

Retrieved: name=Willie, age=6
Instance attribute dictionary: {'name': 'Willie', 'age': 6}

10

Calling Methods on an Instance

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()

Output

Willie is now walking.
Willie is now running!

11

Creating Multiple Distinct Instances

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()

Output

My human is Willie, aged 6.
Willie is now walking.
Your human is Lucy, aged 3.
Lucy is now walking.

12

Modeling Complex Entities: The Car Class

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())

Output

2019 Audi A4

Sources: Coursebook Section 8.2.1 / Eric Matthes, Python Crash Course (2nd ed.)

13

Setting Default Values for Attributes

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()

Output

2019 Audi A4
This car has 0 miles on it.

14

Modifying Attribute Values Directly

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()

Output

This car has 23 miles on it.

15

Modifying Attribute Values via Methods

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()

Output

This car has 23 miles on it.

Sources: Coursebook Section 8.2.3

16

Enforcing Invariants and Validation Logic

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()

Output

This car has 50 miles on it.
Error: You can't roll back an odometer!
This car has 50 miles on it.

17

Incrementing Attribute Values via Methods

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()

Output

This car has 23500 miles on it.
This car has 23600 miles on it.

18

Public Attributes, Protected Conventions, and Name Mangling

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:

1

Public attributes (e.g., self.name)

Intended for direct access by any caller.

2

Protected convention (e.g., self._counter)

Single leading underscore signals internal use. Linters and IDEs warn callers against direct external access, though Python does not block it.

3

Private attribute name mangling (e.g., self.__secret)

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)

Output

Public ID: VAULT-01
Protected state: active
Caught expected error: 'SecurityVault' object has no attribute '__secret_key'
Access via mangled name: RSA_PRIV_KEY_9941

20

Security Application: User Session & Brute-Force Tracking

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)

Output

[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?: True

21

Principles of Inheritance

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).

Key concepts

  • Superclass / Parent class: The generalized class containing common functionality.
  • Subclass / Child class: The specialized class that inherits from the parent and adds or overrides behaviors.

Inheritance hierarchy diagram

[ Base Class: Car ]
(make, model, year, odometer)
          │
  ┌───────┴───────┐
  ▼               ▼
[ ElectricCar ]  [ GasolineCar ]
(battery,        (fuel_tank,
 charging)        emissions)

22

The __init__() Method in Subclasses & super()

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())

Output

2019 Tesla Model S

23

Specializing the Subclass: ElectricCar

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()

Output

Mileage: 12400 miles.

24

Defining Subclass-Specific Attributes & Methods

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()

Output

2019 Tesla Model S
This car has a 75-kWh battery.

Sources: Coursebook Section 8.3.2

25

Overriding Methods from the Parent Class

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()

Output

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

Composition: Instances as Attributes

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()

Output

This car has a 75-kWh battery.

Sources: Coursebook Section 8.3.4

27

Expanding Composed Components

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()

Output

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

Architectural Decision: Inheritance vs. Composition

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

Multiple Inheritance & Method Resolution Order (MRO)

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__)

Output

[AUDIT LOG] Login initiated
Token valid: True
MRO Resolution Order:
 -> AdminSession
 -> Loggable
 -> Authenticable
 -> object

30

Security Application: Role-Based Access Control (RBAC) Hierarchy

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()

Output

User: Duy Tran (@duy_admin)
Assigned Security Privileges:
  [+] rule:allow_ingress_port
  [+] audit:read_raw_pcap
  [+] policy:enforce_lockout

Sources: Coursebook Exercises 8-7 & 8-8 (Admin & Privileges) / NIST SP 800-162 (RBAC Architecture)

31

Modular Architecture for Classes

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.

Module structure overview

project_root/
│
├── car.py              # Class definitions module (Car, Battery, ElectricCar)
├── my_cars.py          # Main driver program
└── security_tools/     # Subpackage
    ├── __init__.py
    └── firewall.py

32

Importing a Single Class

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.

File: car.py

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()

File: main.py

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}")

Output

2019 Audi A4
Current odometer: 23

Sources: Coursebook Section 8.4.1

33

Storing Multiple Classes in a Single Module

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.

Architectural module blueprint (car.py)

"""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 Battery

Cohesion principle: Classes placed in the same module should be modified together for similar reasons. Unrelated classes belong in separate modules.

34

Importing Multiple Specific Classes

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())

Output

2019 Volkswagen Beetle
2019 Tesla Roadster

Engineering 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

Importing an Entire Module

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())

Output

2019 Volkswagen Beetle
2019 Tesla Roadster

Namespace diagram

Global Scope: [ car (module object) ]
              │
              ├── car.Car
              ├── car.Battery
              └── car.ElectricCar

Sources: Coursebook Section 8.4.4

36

Module-to-Module Imports & Avoiding Wildcard Imports

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.

Why wildcard imports are dangerous

  1. Namespace pollution: Injects unknown numbers of identifiers into the local scope.
  1. Silent name shadowing: Can overwrite local functions or variables without warning.
  1. Obscured provenance: Static analysis tools and code reviewers cannot determine where an identifier was defined.

Risky

from car import *

Clean & Explicit

from car import Car, ElectricCar

Namespace Preserving

import car

37

Aliasing Classes and Modules for Clarity

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")

Output

Aliased object instance: 2023 Tesla Model 3
Average network latency: 14 ms

Best practices note: Use standard, community-accepted aliases (e.g., import numpy as np, import pandas as pd).

38

The Standard Library: The random Module

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.

random.randint(a, b)

Returns a random integer N such that a <= N <= b.

random.choice(seq)

Selects a random element from a non-empty sequence.

random.sample(population, k)

Returns a unique k-length selection without replacement.

random.shuffle(seq)

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))

Output

randint(1, 100): 82
Random target host: 10.0.0.1
Sample 2 targets: ['10.0.0.1', '10.0.0.3']

39

Cryptographic Randomness vs. Pseudo-Randomness

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)

Output

Cryptographically secure nonce: e2c94a86f2b0513982cd67a14e9f5e1b

40

The statistics Module for Telemetry Analysis

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))

Output

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.29

41

High-Performance Scientific Computing: NumPy

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.

Memory layout comparison

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())

Output

NumPy array: [10 25 40 55 70]
Shape: (5,)
Mean via vectorization: 40.0

42

NumPy Multi-Dimensional Arrays in Security Analysis

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)

Output

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

Tabular Data Analysis: Introduction to Pandas

Pandas is an open-source data manipulation and analysis library built on top of NumPy.

Provides two primary data structures:

  1. Series: A one-dimensional labeled array capable of holding any data type.
  1. DataFrame: A two-dimensional, tabular data structure with labeled axes (rows and columns).

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)

Output

DataFrame Inspection:
    X   Y   Z
0  78  84  86
1  85  94  97
2  96  89  96
3  80  83  72
4  86  86  83

44

Manipulating DataFrames for Security Intelligence

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']])

Output

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   HTTPS

45

Discovering Modules via Python Module of the Week (PyMOTW)

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.

hashlib / hmac

Cryptographic digests and message authentication.

socket / ssl

Raw network communication and TLS transport encryption.

ipaddress

IP routing, subnet calculation, and CIDR parsing.

subprocess

Executing system binaries safely without shell injection.

path / os / sys

Filesystem and runtime environment inspection.

46

PEP 8 Class Styling & Modern Type Annotations

Professional object-oriented Python conforms to PEP 8 standards and PEP 484 type annotations.

1

Class Naming

Use CapWords (PascalCase), e.g., NetworkDevice, SecurityScanner.

2

Docstrings

Every class must feature a top-level docstring describing its responsibility. Methods follow function docstring rules.

3

Spacing

Surround top-level class definitions with two blank lines. Inside a class, separate methods with a single blank line.

4

Modern Type Annotations

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"))

Output

Rule configured: TCP/443 -> ALLOW
Packet match (443/TCP): True
Packet match (80/TCP):  False

47

Coursebook Practical Exercises Overview

Structured hands-on lab problems from Coursebook Chapter 8 reinforcing OOP mastery:

1

Exercises 8-1 to 8-3 (Foundations)

  • 8-1 & 8-2 Restaurant: Model restaurant attributes and describe_restaurant() across multiple instances.
  • 8-3 Users: Model first_name, last_name, and greet_user().
2

Exercises 8-4 & 8-5 (State Modification)

  • 8-4 Number Served: Implement set_number_served() and increment_number_served().
  • 8-5 Login Attempts: Implement increment_login_attempts() and reset_login_attempts().
3

Exercises 8-6 to 8-9 (Inheritance & Composition)

  • 8-6 Ice Cream Stand & 8-7 Admin: Create subclasses inheriting from Restaurant and User.
  • 8-8 Privileges: Decompose Admin by creating a standalone Privileges class as a composed attribute.
  • 8-9 Battery Upgrade: Add upgrade_battery() and verify updated driving range.
4

Exercises 8-10 to 8-16 (Modules & Libraries)

  • 8-10 to 8-12: Store classes across single and multiple modules; test imports.
  • 8-13 to 8-15 (Dice & Lottery): Model stochastic events using random.randint() and random.choice().
  • 8-16: Explore PyMOTW documentation.

Sources: Coursebook Section "Bài tập chương 8" (Exercises 8-1 to 8-16)

48

Comprehensive Security Lab: Packet Inspection Engine

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)

Output

[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: 2

Sources: Coursebook Chapter 8 OOP Synthesis / Python for Cybersecurity (Wiley, 2022)

49

Session Synthesis & Course Roadmap

Summary of core paradigms mastered in Session 5:

1

Object-Oriented Encapsulation

Bundling state (attributes) and behavior (methods) into classes, instantiated via __init__(self).

2

Guarded State Management

Modifying attributes via accessor/mutator methods to enforce invariants and prevent illegal transitions.

3

Reusable Hierarchies

Leveraging single inheritance with super() and polymorphic method overriding.

4

Architectural Composition

Decomposing complex software into interacting components using instances as attributes.

5

Modular Deployment & Core Libraries

Distributing classes across modules and applying standard/scientific libraries (random, statistics, NumPy, Pandas).

Roadmap Preview (Next Session — Chapter 9: File I/O & Robust Exception Handling)

  • Persisting state to disk using context managers (with open(...) as f:).
  • Handling system anomalies gracefully with try-except-else-finally blocks.
  • Serializing structured datasets using JSON.

50

Two Object Models: C++ vs. Python at a Glance

  • Learners arrive from INT1339 (C++ OOP) with a compiled, statically typed object model. Python implements the same OOP pillars (encapsulation, inheritance, polymorphism, abstraction) on top of a dynamic, interpreted runtime.
  • This section maps each C++ mechanism from Coursebook C++ Chapters 5–6 onto its Python counterpart, highlighting what changes and why it matters for security tooling.

51

Access Control: Compiler-Enforced vs. Convention-Based

  • C++ access specifiers are checked by the compiler: any access to a private member from outside the class (except by friends) is a compile-time error.
  • Python has no compile-time access enforcement. It relies on naming conventions and name mangling (see the SecurityVault example earlier in this deck).
  • C++ friend functions/classes grant selective access to private members. Python has no friend keyword, since any code can technically reach any attribute.

C++17

#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 = 17

Python 3

Security takeaway

Neither 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

Constructors, Destructors & Resource Lifetime

  • C++ constructors share the class name and support overloading. Destructors (~ClassName) run deterministically when an object leaves scope. This is the basis of RAII (Resource Acquisition Is Initialization).
  • Python separates construction (__new__) from initialization (__init__) and allows only one __init__ per class (default arguments replace overloading).
  • Python's __del__ is a finalizer, not a deterministic destructor: its timing depends on reference counting and the garbage collector. Deterministic cleanup in Python uses context managers (with statement, __enter__/__exit__).

C++17

#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;
}

Python 3

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")

Output (identical for both programs)

Enter scope
Open session: analyst
Working...
Close session: analyst
Left scope

53

Object Pointers, Object Arrays & Memory Safety

  • In C++, an object can be a value on the stack, a raw pointer to heap memory (new/delete), or an owner through a smart pointer (std::unique_ptr). The programmer chooses and is responsible for lifetime.
  • In Python, every variable is a reference to a heap object. There is no new/delete, no -> operator, and no pointer arithmetic. Assignment copies the reference, not the object.

C++17

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.5

Python 3

class 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.5
Security takeaway
  • Manual memory management in C++ makes possible vulnerability classes such as use-after-free (CWE-416), double free (CWE-415), and buffer overflow (CWE-787). Pure Python code is memory-safe in this sense; Python's risk shifts to logic errors such as unintended aliasing (h1 and h2 above).
  • The NSA Cybersecurity Information Sheet "Software Memory Safety" (2022) lists Python among memory-safe languages.

54

Inheritance Declaration & Derivation Access Modes

  • C++ derivation syntax includes an access mode: class Derived : public | protected | private Base. The mode changes how inherited members are visible to users of the derived class.
  • Python has a single inheritance mode: class Derived(Base):. All attributes are inherited and remain accessible; there is no equivalent of protected or private inheritance.
  • C++ private inheritance models "implemented-in-terms-of". In Python, the same intent is expressed with composition (see the ElectricCar/Battery slides).

C++17

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 on

Python 3

class 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

Constructor & Destructor Chaining in Inheritance

  • In C++, the base-class constructor always runs before the derived constructor body, and is invoked through the member initializer list. Destructors run in exact reverse order. The compiler guarantees this ordering.
  • In Python, the base __init__ runs only if the subclass calls super().__init__() explicitly. The call can occur anywhere in the method body. If it is omitted, base attributes are never created.

C++17

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() destructor

Python 3

class 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

Method Overriding, Upcasting & Object Slicing

  • C++ allows a derived object to be converted to its base type (upcasting). Copying a derived object into a base-class value causes object slicing: derived-only members are discarded. Without virtual, a call through a base pointer runs the base version (static binding).
  • Python never slices objects. Assignment only binds another name to the same object, so the most-derived method always runs. A type hint such as p: Packet does not change the object.

C++17

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.9

Python 3

class 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 TcpPacket

57

Polymorphism: Virtual Functions vs. Dynamic Dispatch & Duck Typing

  • C++ achieves runtime polymorphism only for functions declared virtual. Calls are resolved through a per-class virtual method table (vtable). The override keyword asks the compiler to verify the signature.
  • In Python, every method call is resolved at runtime by attribute lookup along the MRO, so every method behaves like a C++ virtual function. No keyword is needed.
  • Python also supports duck typing: objects do not need a common base class, only a method with the expected name ("if it walks like a duck and quacks like a duck...").

C++17

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 scan

Python 3 (duck typing, no shared base class)

class 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 scan

58

Abstract Base Classes: Pure Virtual Functions vs. abc.ABC

  • In C++, a class with at least one pure virtual function (= 0) is abstract. Instantiating it is a compile-time error. C++ interfaces are abstract classes containing only pure virtual functions.
  • Python provides the abc module. A class derived from ABC with methods decorated by @abstractmethod cannot be instantiated. The check happens at runtime, when the object is created, and raises TypeError.

C++17

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: false

Python 3

from 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: False

59

Multiple Inheritance & the Diamond Problem

  • Both languages support multiple inheritance, and both must handle the diamond: UTMAppliance derives from Firewall and Router, which both derive from Device.
  • C++ by default creates two Device subobjects (ambiguous access). Declaring virtual public Device shares a single base subobject.
  • Python always shares a single base. Method lookup and cooperative super() calls follow the MRO computed by C3 linearization, so Device.__init__ runs exactly once.

C++17

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 id

Python 3

class 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