Table of Contents Link to heading
- Variable Scope in Python Classes
- Class Variables
- Instance Variables
- Attribute Lookup Order
- Class Methods: @classmethod
- Static Methods: @staticmethod
- When to Use Each
Variable Scope in Python Classes Link to heading
Python classes have two variable scopes that are frequently confused because both can be accessed using the same dot notation on an instance:
- Class variables — defined at class level; one copy shared by the class and all its instances
- Instance variables — defined in methods (typically
__init__) usingself; each instance has its own independent copy
Understanding which type a variable is determines whether changing it on one instance affects other instances.
Class Variables Link to heading
Class variables are defined inside the class body but outside any method. They are associated with the class object itself, not with any instance.
class NetworkDevice:
vendor = "Cisco" # class variable
total_devices = 0 # class variable used as a counter
def __init__(self, hostname):
self.hostname = hostname # instance variable
NetworkDevice.total_devices += 1
Shared State Across All Instances Link to heading
All instances access the same class variable — changes through the class object propagate to all instances (unless an instance has shadowed the variable):
d1 = NetworkDevice("router-01")
d2 = NetworkDevice("router-02")
print(NetworkDevice.vendor) # Cisco
print(d1.vendor) # Cisco (found on class, no instance shadow)
print(d2.vendor) # Cisco
NetworkDevice.vendor = "Juniper"
print(d1.vendor) # Juniper (all instances see the change)
print(d2.vendor) # Juniper
print(NetworkDevice.total_devices) # 2 (counter incremented by both __init__ calls)
Assigning to an instance variable with the same name shadows the class variable for that instance only:
d1.vendor = "Arista" # creates an instance variable, shadows class variable
print(d1.vendor) # Arista (instance variable takes precedence)
print(d2.vendor) # Juniper (still sees class variable)
print(NetworkDevice.vendor) # Juniper (class variable unchanged)
del d1.vendor # remove the instance variable
print(d1.vendor) # Juniper (class variable visible again)
The Mutable Class Variable Pitfall Link to heading
The most common OOP mistake in Python: using a mutable object (list, dict) as a class variable when per-instance behaviour was intended.
class Router:
interfaces = [] # WRONG: shared list — all instances share the same list
r1 = Router()
r2 = Router()
r1.interfaces.append("Gi0/0")
print(r2.interfaces) # ['Gi0/0'] — r2 is affected!
The fix: initialise mutable attributes in __init__:
class Router:
def __init__(self):
self.interfaces = [] # CORRECT: each instance gets its own list
r1 = Router()
r2 = Router()
r1.interfaces.append("Gi0/0")
print(r2.interfaces) # [] — r2 is unaffected
__init__.Instance Variables Link to heading
Instance variables are defined by assigning to self.name inside a method. Each instance maintains its own independent namespace for these variables.
class Switch:
model = "Catalyst 9300" # class variable
def __init__(self, hostname, ip_address):
self.hostname = hostname # instance variable
self.ip_address = ip_address # instance variable
self.vlans = [] # instance variable — mutable, per instance
self.uptime_seconds = 0 # instance variable
def add_vlan(self, vlan_id, name):
self.vlans.append({"id": vlan_id, "name": name})
return self
s1 = Switch("access-01", "10.0.1.1")
s2 = Switch("access-02", "10.0.1.2")
s1.add_vlan(10, "DATA").add_vlan(20, "VOICE")
s1.uptime_seconds = 3600
print(s1.vlans) # [{'id': 10, 'name': 'DATA'}, {'id': 20, 'name': 'VOICE'}]
print(s2.vlans) # [] — independent
print(s1.uptime_seconds) # 3600
print(s2.uptime_seconds) # 0 — independent
Attribute Lookup Order Link to heading
When you access instance.attribute, Python follows this lookup chain:
- The instance’s own
__dict__(instance variables) - The class’s
__dict__(class variables) - Base classes in Method Resolution Order (MRO) — relevant for inheritance
print(d1.__dict__) # instance's own attributes
print(NetworkDevice.__dict__) # class attributes
This lookup order is why an instance can shadow a class variable: when found in step 1, the search stops before reaching step 2.
Class Methods: @classmethod Link to heading
A class method receives the class as its first argument (cls) rather than an instance. It can access and modify class-level state.
Common use: alternative constructors that create instances from different input formats.
class NetworkDevice:
_registry = {} # class-level device registry
def __init__(self, hostname, ip_address, device_type):
self.hostname = hostname
self.ip_address = ip_address
self.device_type = device_type
NetworkDevice._registry[hostname] = self
@classmethod
def from_dict(cls, config_dict):
"""Alternative constructor: create from a config dictionary."""
return cls(
hostname=config_dict["hostname"],
ip_address=config_dict["ip"],
device_type=config_dict["type"]
)
@classmethod
def get_device(cls, hostname):
"""Look up a device from the class registry."""
return cls._registry.get(hostname)
@classmethod
def device_count(cls):
return len(cls._registry)
# Standard constructor
d1 = NetworkDevice("router-01", "10.0.0.1", "router")
# Alternative constructor via classmethod
config = {"hostname": "switch-01", "ip": "10.0.0.2", "type": "switch"}
d2 = NetworkDevice.from_dict(config)
print(NetworkDevice.device_count()) # 2
print(NetworkDevice.get_device("router-01").ip_address) # 10.0.0.1
Static Methods: @staticmethod Link to heading
A static method receives neither self nor cls. It behaves like a regular function that happens to live inside a class namespace — used when a method is logically related to the class but does not need to access instance or class state.
class IPUtils:
@staticmethod
def validate_ipv4(address):
"""Return True if address is a valid IPv4 address."""
parts = address.split(".")
if len(parts) != 4:
return False
return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)
@staticmethod
def to_binary(octet):
"""Convert a decimal octet to 8-bit binary string."""
return format(int(octet), "08b")
# Called on the class (no instance needed)
print(IPUtils.validate_ipv4("192.168.1.1")) # True
print(IPUtils.validate_ipv4("300.0.0.1")) # False
print(IPUtils.to_binary("192")) # 11000000
# Can also be called on an instance
utils = IPUtils()
print(utils.validate_ipv4("10.0.0.1")) # True
When to Use Each Link to heading
| Variable / Method Type | Use When |
|---|---|
| Class variable | Data shared across all instances — counters, constants, registries, default configuration |
| Instance variable | Data unique to each instance — hostname, IP, state, mutable collections |
Instance method (self) |
Logic that reads or modifies instance state |
Class method (@classmethod, cls) |
Alternative constructors; factory methods; class-level operations |
Static method (@staticmethod) |
Utility functions logically related to the class but requiring no instance or class state |
self or cls, it can be a @staticmethod. If it references the class but no specific instance, it can be a @classmethod. If it references instance state, it must be an instance method.