Study guide

6.3 Class Methods, Static Methods, and Properties

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

6.3 Class Methods, Static Methods, and Properties

Key Takeaways Explain This Topic AI

Instance methods receive self as their implicit first parameter and operate directly on instance state and class attributes.

Class methods are decorated with @classmethod , receive the class object cls as their implicit first parameter, and serve as alternative constructors / factory methods that support inheritance.

Static methods are decorated with @staticmethod , receive no implicit first argument ( self or cls ), and function as plain utility functions namespaced within a class.

The @property decorator transforms methods into managed attributes, supporting getter, setter ( @name.setter ), and deleter ( @name.deleter ) logic while retaining standard dot notation syntax.

The __slots__ class variable restricts attribute creation, eliminates the per-instance __dict__ dictionary, and drastically reduces memory consumption.

Last updated: August 2026

Class Methods, Static Methods, and Properties

In standard Python class definitions, methods are bound to specific object instances. However, real-world object-oriented architecture requires a diverse toolkit of method types and encapsulation mechanisms to manage class-level data, implement factory constructors, provide namespaced utilities, and enforce strict attribute validation.

Python provides three primary built-in method decorators ( @classmethod , @staticmethod , and @property ) along with the memory optimization mechanism __slots__ to fine-tune how classes and instances interact with data.

1. Instance Methods vs. Class Methods vs. Static Methods

Every method declared inside a class body is classified based on how the Python descriptor protocol binds its arguments when invoked.

A. Instance Methods ( self )

Instance methods are the default method type in Python. When invoked on an instance, Python automatically passes the instance itself as the implicit first argument ( self ): class BankAccount:

def __init__(self, account_holder, balance=0.0):

  • self.account_holder = account_holder
  • self.balance = balance

# Instance Method: Bound to the instance (self)

def deposit(self, amount):

if amount <= 0:

  • raise ValueError("Deposit amount must be positive.")
  • self.balance += amount
  • return self.balance

Binding: Bound to the specific instance ( acc.deposit(100) is syntactic sugar for BankAccount.deposit(acc, 100) ).

Access: Can access and modify both instance state ( self.balance ) and class state ( self.__class__.interest_rate ).

B. Class Methods ( @classmethod and cls )

A class method is decorated with @classmethod . When invoked, Python automatically passes the class object itself (the type) as the implicit first argument, conventionally named cls : class BankAccount:

interest_rate = 0.035 # Class attribute (3.5% APY)

def __init__(self, account_holder, balance=0.0):

  • self.account_holder = account_holder
  • self.balance = balance
  • # Class Method: Bound to the class (cls)
  • @classmethod

def set_interest_rate(cls, new_rate):

if new_rate < 0:

  • raise ValueError("Rate cannot be negative.")
  • cls.interest_rate = new_rate
  • # Alternative Constructor / Factory Method
  • @classmethod

def from_csv_string(cls, data_row):

  • """Create a BankAccount instance from a 'Name,Balance' string."""
  • name, balance_str = data_row.split(",")
  • # Uses cls(...) so subclasses instantiate the correct derived type
  • return cls(name.strip(), float(balance_str.strip()))
  • Calling Semantics

Class methods can be called directly on the class ( BankAccount.set_interest_rate(0.04) ) or on an instance ( acc.set_interest_rate(0.04) ). In both cases, cls receives the class object ( BankAccount ), never the instance.

Why Factory Methods Use cls(...) Instead of ClassName(...)

When an alternative constructor uses cls(...) , it supports subclass polymorphism . If a subclass SavingsAccount inherits from_csv_string() , calling SavingsAccount.from_csv_string("Bob,500") automatically constructs an instance of SavingsAccount , rather than hardcoding BankAccount .

C. Static Methods ( @staticmethod )

A static method is decorated with @staticmethod . Static methods receive no implicit first argument —neither self nor cls is passed: class BankAccount:

  • # Static Method: Plain function logically namespaced within the class
  • @staticmethod

def validate_routing_number(routing_number):

  • """Utility function checking if routing number is a 9-digit string."""
  • return isinstance(routing_number, str) and len(routing_number) == 9 and routing_number.isdigit()
  • # Invocation on Class or Instance
  • print(BankAccount.validate_routing_number("123456789")) # Output: True
  • print(BankAccount.validate_routing_number("invalid")) # Output: False

Static methods are used when a utility function is conceptually related to the class domain, but does not need to read or modify instance attributes or class attributes.

Comparison Table: Python Method Types

  • Feature Instance Method Class Method ( @classmethod ) Static Method ( @staticmethod )
  • Decorator None (Default) @classmethod @staticmethod
  • First Argument self (Instance) cls (Class / Type) None (Plain arguments only)
  • Implicit Binding Bound to instance Bound to class Unbound (Plain function)
  • Can Access Instance State ( self )? Yes No No
  • Can Access Class State ( cls )? Yes (via self.__class__ ) Yes (via cls ) No (unless hardcoded)

Primary Use Case Modifying object state & behavior Factory constructors, class-level config Utility functions & validators

2. Managed Attributes with @property

In many object-oriented languages (like Java or C++), developers write explicit get_x() and set_x() methods to protect private variables. In Python, the idiomatic convention is to expose public attributes directly ( obj.x ), and convert them to managed properties using @property if validation or computed behavior is required later—without breaking the external public API.

Property Getters, Setters, and Deleters

The @property decorator turns a method into a read-only getter. Additional decorators ( @name.setter and @name.deleter ) define mutation and deletion logic: class Temperature:

def __init__(self, celsius=0.0):

  • # Calls the property setter below to enforce validation
  • self.celsius = celsius
  • # 1. GETTER: Accessed like an attribute (temp.celsius)
  • @property

def celsius(self):

return self._celsius

  • # 2. SETTER: Triggered on assignment (temp.celsius = 25)
  • @celsius.setter

def celsius(self, value):

if not isinstance(value, (int, float)):

raise TypeError("Temperature must be numeric.")

if value < -273.15:

  • raise ValueError("Temperature cannot be below absolute zero (-273.15°C)!")
  • self._celsius = float(value)
  • # 3. DELETER: Triggered on del (del temp.celsius)
  • @celsius.deleter

def celsius(self):

  • print("Resetting temperature to 0.0°C...")
  • self._celsius = 0.0
  • # Computed Read-Only Property (No setter defined!)
  • @property

def fahrenheit(self):

  • return (self._celsius * 9 / 5) + 32
  • Operational Behavior t = Temperature(25)
  • print(t.celsius) # Output: 25.0 (Invokes getter)
  • print(t.fahrenheit) # Output: 77.0 (Invokes computed getter)
  • t.celsius = 100 # Invokes setter with validation
  • print(t.fahrenheit) # Output: 212.0

# Read-Only Property Enforcement:

try:

t.fahrenheit = 500 # No @fahrenheit.setter exists!

except AttributeError as err:

print("AttributeError:", err) # AttributeError: property 'fahrenheit' of 'Temperature' object has no setter

# Validation Enforcement:

try:

t.celsius = -300

except ValueError as err:

print("Validation error:", err) # Validation error: Temperature cannot be below absolute zero...

3. Memory Optimization with __slots__

By default, every Python instance stores its instance attributes inside a private dictionary accessible via instance.__dict__ . While this enables dynamic attribute creation at runtime ( obj.new_attr = 123 ), dictionaries incur noticeable memory overhead due to hash table allocation.

When an application creates millions of small objects (such as points in 3D geometry or financial market ticks), dictionary overhead can exhaust system memory.

The __slots__ Declaration

Defining __slots__ at class scope instructs Python to allocate a fixed-size array of descriptor references instead of creating a __dict__ for each instance: class StandardPoint:

def __init__(self, x, y):

  • self.x = x
  • self.y = y

class SlottedPoint:

__slots__ = ('x', 'y') # Sequence of allowable attribute strings

def __init__(self, x, y):

  • self.x = x
  • self.y = y
  • p1 = StandardPoint(10, 20)
  • p2 = SlottedPoint(10, 20)
  • print(hasattr(p1, '__dict__')) # Output: True (Has instance dictionary)
  • print(hasattr(p2, '__dict__')) # Output: False (No __dict__ allocated!)
  • Key Mechanics and Restrictions of __slots__

Memory Efficiency: Reduces per-instance memory consumption by 40% to 60% by eliminating __dict__ and __weakref__ .

Faster Access: Attribute lookups on slotted classes are measurably faster than dictionary lookups.

Attribute Restriction: Instances are strictly prohibited from creating attributes not explicitly declared in __slots__ : p2.z = 30 # Raises AttributeError: 'SlottedPoint' object has no attribute 'z'

Inheritance Caveat: Subclasses do not inherit the slotted behavior automatically. If a subclass of SlottedPoint does not define its own __slots__ , Python will automatically allocate a __dict__ for subclass instances.

Loading diagram... Method Dispatch and Parameter Binding Architecture