A class is a blueprint. An object (instance) is a concrete thing built from that blueprint, holding its own state.
Defining a Class
class Dog: species = "Canis familiaris" # class attribute, shared by ALL instances def __init__(self, name, age): self.name = name # instance attribute, unique per object self.age = age def bark(self): return f"{self.name} says Woof!"rex = Dog("Rex", 3)buddy = Dog("Buddy", 5)rex.bark() # 'Rex says Woof!'rex.name # 'Rex'rex.species # 'Canis familiaris', inherited from the class
self: The Instance Reference
self is the first parameter of every instance method, referring to the specific object the method was called on. Python passes it automatically, rex.bark() is really Dog.bark(rex) under the hood.
class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 # 'self' is how the method reaches into ITS OWN datac1 = Counter()c2 = Counter()c1.increment()c1.increment()print(c1.count, c2.count) # 2 0 -- each instance has its own independent state
__init__: The Constructor
Called automatically when a new instance is created. Its job is to set up initial state.
class Employee: company = "Acme" # class attribute: ONE copy shared across all instances def __init__(self, name): self.name = name # instance attribute: separate per instancee1 = Employee("Bob")e2 = Employee("Amy")Employee.company = "NewCo" # changes the shared class attributeprint(e1.company, e2.company) # NewCo NewCo, both see the changee1.company = "Solo Inc" # creates a NEW instance attribute on e1 ONLY, # shadows the class attribute for e1 aloneprint(e1.company, e2.company) # Solo Inc NewCo
Mutable class attributes are shared and dangerous
class Team: members = [] # DANGER: shared list across ALL Team instances def add(self, name): self.members.append(name)t1 = Team()t2 = Team()t1.add("Bob")print(t2.members) # ['Bob'] -- leaked into t2 as well!
Fix: initialize mutable attributes inside __init__, not as class attributes.
class Team: def __init__(self): self.members = []
Instance Methods
Regular methods take self and operate on/read instance state.
By default, printing an object shows an unhelpful memory address. Define __repr__/__str__ to fix this. See Magic-Dunder-Methods for the complete list.
class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})"p = Point(3, 4)print(p) # Point(3, 4), instead of <__main__.Point object at 0x...>
Deleting Attributes and Objects
del rex.age # removes the 'age' attribute from the instancedel rex # removes the reference; the object is garbage collected once unreferenced
__slots__: Restricting Attributes for Memory Efficiency
By default, every instance carries a __dict__ for arbitrary attribute storage. __slots__ skips this, saving memory when creating many instances.
class Point: __slots__ = ("x", "y") # only these attributes are allowed, no others def __init__(self, x, y): self.x = x self.y = yp = Point(1, 2)p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
When __slots__ matters
Worth using when creating millions of small objects (memory-sensitive data processing), rarely necessary for everyday application code.