Study guide

5.1 Classes, Instances, and Attributes

OpenEDG PCAP — Certified Associate in Python Programming Exam

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: The PCAP-31-03 Exam Blueprint and Test-Day Logistics

1.1 The PCAP-31-03 Blueprint, Scoring, and Exam-Day Logistics

2 Chapter 2: Modules, Packages, and the Standard Library

2.1 Module Imports and Namespace Mechanics 2.2 Module Construction and Execution 2.3 Package Hierarchy and Navigation 2.4 Standard Library Modules and Pip

3 Chapter 3: Exceptions and Error Handling

3.1 Exception Control Flow and Syntax 3.2 The Built-in Exception Hierarchy 3.3 Raising Exceptions and Assertions 3.4 Custom Exceptions and Exception Chaining

4 Chapter 4: Strings, Encodings, and Text Processing

4.1 Character Encodings and String Immutability 4.2 String Indexing, Slicing, and Comparison 4.3 String Inspection and Classification Methods 4.4 String Transformation and Formatting Methods 4.5 Iterating, Sorting, and Comparing Strings Against Other Types

5 Chapter 5: Object-Oriented Programming Fundamentals

5.1 Classes, Instances, and Attributes 5.2 Encapsulation, Privacy, and Name Mangling 5.3 Dunder Methods and Object Lifecycle 5.4 Discovering Class Structure: __name__, __module__, __bases__, and Identity

6 Chapter 6: Advanced OOP, Inheritance, and Metaprogramming

6.1 Single Inheritance, Method Overriding, and Polymorphism 6.2 Multiple Inheritance and Method Resolution Order (MRO) 6.3 Class Methods, Static Methods, and Properties

7 Chapter 7: Functional Python, Comprehensions, and Closures

7.1 List, Dictionary, and Set Comprehensions 7.2 Lambda Expressions and Higher-Order Functions 7.3 Variable Scopes, the LEGB Rule, and Closures

8 Chapter 8: Iterators, Generators, and File I/O

8.1 Iterators and the Iteration Protocol 8.2 Generator Functions and Generator Expressions 8.3 File Input/Output and Context Managers 8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

5.1 Classes, Instances, and Attributes

Key Takeaways Explain This Topic AI

A class is a user-defined blueprint defining data and behaviors, while an instance is a concrete, distinct runtime object allocated in memory.

Instantiation invokes __new__() to allocate memory and __init__(self, ...) to initialize instance attributes, returning the newly created instance.

The self parameter is an explicit reference to the instance bound automatically during method calls, translating obj.method(arg) into ClassName.method(obj, arg) .

Instance variables are unique to each instance and stored in self.__dict__ , whereas class variables are defined in the class body and shared across all instances via ClassName.__dict__ .

Attribute lookup searches the instance namespace first before checking the class namespace; assigning to self.var when var is a class attribute creates a new instance variable that shadows the class attribute without altering the class default.

Last updated: August 2026

Classes, Instances, and Attributes

Object-Oriented Programming (OOP) is a foundational programming paradigm in Python that structures software design around data models, known as objects , rather than functions and sequential logic alone. In Python, object-oriented concepts are deeply integrated into the runtime architecture: nearly every construct—including integers, strings, lists, functions, and modules—is an object instantiated from a corresponding class.

1. The OOP Paradigm in Python: Classes vs. Instances

To understand object orientation in Python, you must maintain a strict conceptual distinction between a class and an instance :

Class (Blueprint / Type) : A class is a programmer-defined template or schema that bundles data (attributes) and behavior (methods) together. It defines what state its instances can hold and what operations they can perform.

Instance (Object / Realization) : An instance is a concrete, individual realization of a class created during runtime. Each instance occupies its own dedicated block of memory, maintains its own independent state, and shares method definitions with all other instances of that class. # Checking types and object identities

class Server:

pass

  • server_alpha = Server()
  • server_beta = Server()
  • print(type(server_alpha)) # <class '__main__.Server'>
  • print(type(server_alpha) is Server) # True
  • print(server_alpha is server_beta) # False (distinct memory locations)
  • print(hex(id(server_alpha))) # e.g., 0x104b2a8d0
  • print(hex(id(server_beta))) # e.g., 0x104b2a950
  • 2. The class Keyword and Class Definition

A class is defined using the class keyword followed by the class identifier and a colon. The body of the class is indented and executed immediately upon definition to build the class object. class Device:

  • """A class representing a managed network hardware device."""
  • device_category = "Hardware" # Class attribute evaluated at definition time
  • # Inspecting the class object itself
  • print(Device.__name__) # 'Device'
  • print(Device.__doc__) # 'A class representing a managed network hardware device.'

By convention (PEP 8), class names use CapWords (PascalCase) naming (e.g., NetworkSwitch , UserProfile ), distinguishing them from lowercase function and variable names.

3. Object Instantiation and the __init__() Initializer

Creating an instance of a class is called instantiation . Instantiation is performed by calling the class name as if it were a function: instance_ref = ClassName ( arguments... ) \text{instance\_ref} = \text{ClassName}(\text{arguments...}) instance_ref = ClassName ( arguments... )

The Two-Stage Object Creation Process

When Python executes ClassName(*args, **kwargs) , it performs a two-stage sequence under the hood:

Memory Allocation ( __new__ ) : Python calls the static allocator method __new__(cls, *args, **kwargs) to create an empty, uninitialized object in memory.

Initialization ( __init__ ) : Python immediately passes the newly created object as the first argument ( self ) to __init__(self, *args, **kwargs) to initialize its attributes. class Packet:

def __init__(self, source_ip, destination_ip, payload_size):

  • # Initialize instance attributes
  • self.source_ip = source_ip
  • self.destination_ip = destination_ip
  • self.payload_size = payload_size
  • self.is_encrypted = False
  • # Instantiating two independent Packet objects
  • p1 = Packet("192.168.1.10", "10.0.0.1", 1024)
  • p2 = Packet("172.16.0.5", "8.8.8.8", 512)
  • print(p1.source_ip, p1.payload_size) # 192.168.1.10 1024
  • print(p2.source_ip, p2.payload_size) # 172.16.0.5 512
  • The Strict Return Rule of __init__

A critical rule tested on the PCAP exam is that __init__() must return None (or omit an explicit return statement). Returning any value other than None from __init__() raises a TypeError at runtime: class FaultyClass:

def __init__(self, value):

self.value = value

return 42 # ILLEGAL: TypeError raised during instantiation!

try:

obj = FaultyClass(10)

except TypeError as err:

  • print("Instantiation Error:", err)
  • # Output: Instantiation Error: __init__() should return None, not 'int'
  • 4. The self Parameter Mechanism

Unlike languages like C++ or Java where the object reference ( this ) is implicit, Python requires the reference to the active object instance to be explicitly declared as the first parameter in all regular instance method signatures.

Parameter Declaration vs. Method Invocation

In Definition : self must be written as the first parameter of every instance method.

In Invocation : Python automatically binds the calling instance and passes it as self . You do not supply an argument for self . class Account:

def __init__(self, account_id, initial_balance=0.0):

  • self.account_id = account_id
  • self.balance = initial_balance

def deposit(self, amount):

  • self.balance += amount
  • return self.balance

def get_summary(self):

return f"Account {self.account_id}: Balance ${self.balance:.2f}"

acc = Account("ACC-9021", 150.0)

  • # Syntactic Sugar Invocation
  • acc.deposit(50.0)

# Behind the scenes, Python translates the above to:

# Account.deposit(acc, 50.0)

  • print(acc.get_summary()) # Account ACC-9021: Balance $200.00
  • Equivalence of Invocation Forms

The expression instance.method(arg) is syntactic sugar for Class.method(instance, arg) . Both forms are completely valid in Python: # Method called via instance (implicit self binding)

result1 = acc.deposit(25.0)

  • # Method called via Class (explicit instance argument passed to self)
  • result2 = Account.deposit(acc, 25.0)

print(result1, result2) # 225.0 250.0

Naming Convention: While self is not a reserved Python keyword (syntactically, any valid identifier like this , me , or obj works), PEP 8 mandates the name self . Using anything other than self is considered non-idiomatic and bad practice.

5. Instance Variables vs. Class Variables

Python maintains two distinct categories of attributes within object models:

Feature Instance Variables Class Variables

Definition Location Inside methods using self.var = value (typically in __init__ ) Directly inside the class body, outside all methods

Storage Location Stored in the instance dictionary ( instance.__dict__ ) Stored in the class dictionary ( ClassName.__dict__ )

Scope & Lifetime Unique to that specific instance; allocated when created Shared across all instances of the class; allocated on definition

Access Mechanism Accessible via self.var or instance.var Accessible via ClassName.var or instance.var

Primary Use Case Unique object state (e.g., ID, balance, username, dimensions) Shared constants, default configurations, or instance counters class DatabaseConnection:

  • # Class variables (shared state)
  • default_port = 5432
  • active_connections = 0

def __init__(self, host, database):

  • # Instance variables (unique state)
  • self.host = host
  • self.database = database
  • DatabaseConnection.active_connections += 1
  • conn1 = DatabaseConnection("db1.internal", "users")
  • conn2 = DatabaseConnection("db2.internal", "orders")
  • print(conn1.host, conn1.default_port) # db1.internal 5432
  • print(conn2.host, conn2.default_port) # db2.internal 5432
  • print("Total connections:", DatabaseConnection.active_connections) # 2
  • 6. Attribute Lookup Hierarchy and Namespace Resolution

When you access an attribute on an instance using dot notation ( instance.attribute ), Python resolves the symbol through a deterministic lookup pipeline:

Step 1: Check Instance Namespace : Python inspects instance.__dict__ . If the key 'attribute' exists, its value is returned immediately.

Step 2: Check Class Namespace : If not found in the instance, Python checks instance.__class__.__dict__ (the class where the instance was spawned).

Step 3: Check Base Classes : If not found in the class, Python searches parent base classes according to the Method Resolution Order (MRO).

Step 4: Raise Error : If the attribute is nowhere in the hierarchy, Python raises an AttributeError . Access: obj.attr

  • │
  • ▼
  • [1. In obj.__dict__?] ──▶ YES ──▶ Return obj.__dict__['attr']
  • │ NO
  • ▼
  • [2. In Class.__dict__?] ──▶ YES ──▶ Return Class.__dict__['attr']
  • │ NO
  • ▼
  • [3. In Base Classes?] ──▶ YES ──▶ Return BaseClass.__dict__['attr']
  • │ NO
  • ▼
  • [4. Raise AttributeError]
  • 7. Attribute Shadowing and Mutation Gotchas

One of the most heavily tested areas on the PCAP exam is the subtle difference between reading a class variable through an instance versus assigning to a class variable through an instance.

The Shadowing Mechanism

When you read inst.var , Python falls back to the class variable if var is not in inst.__dict__ . However, when you assign inst.var = new_value , Python does not modify the class variable. Instead, it creates a new instance variable in inst.__dict__ that shadows (hides) the class variable for that specific instance. class Config:

timeout = 30 # Class variable

  • c1 = Config()
  • c2 = Config()

print(c1.timeout, c2.timeout, Config.timeout) # 30 30 30

  • # Modifying through instance c1
  • c1.timeout = 60

# What happened?

  • print(c1.timeout) # 60 (reads from c1.__dict__['timeout'])
  • print(c2.timeout) # 30 (falls back to Config.__dict__['timeout'])
  • print(Config.timeout) # 30 (class variable remains unchanged!)
  • # Verify internal dictionaries
  • print(c1.__dict__) # {'timeout': 60}
  • print(c2.__dict__) # {}
  • print('timeout' in Config.__dict__) # True
  • Modifying Class Variables Directly

To modify a class variable so that the change reflects across all instances (that have not shadowed the attribute), you must assign to the class attribute directly via the class name: Config.timeout = 45

  • print(c1.timeout) # 60 (still shadowed by c1's instance variable)
  • print(c2.timeout) # 45 (reflects the updated class variable)
  • The Mutable Class Variable Trap

If a class variable holds a mutable object (such as a list, dictionary, or set), modifying the object in-place via an instance (e.g., inst.items.append(val) ) mutates the single shared object in memory for all instances: class Inventory:

shared_items = [] # Class variable containing a mutable list!

def __init__(self, owner):

self.owner = owner

  • inv1 = Inventory("Warehouse A")
  • inv2 = Inventory("Warehouse B")
  • # In-place mutation does not assign a new attribute; it mutates the shared list
  • inv1.shared_items.append("Widget")

print(inv1.shared_items) # ['Widget']

print(inv2.shared_items) # ['Widget'] -- Unexpected shared state!

Best Practice: To give each instance its own independent collection, always initialize mutable data structures inside __init__() using self.items = [] .

Loading diagram... Attribute Lookup Resolution and Shadowing Mechanism