A class is a blueprint that says which attributes and behaviors its objects will have. An object is one concrete instance built from that blueprint. Almost everything else in OOP builds on this, so it pays to know exactly where an object's data lives and how Python creates it.
Classes and objects#
Think of a cookie cutter: the cutter is the class, and each cookie is an object.
class Dog:
pass
d1 = Dog()
d2 = Dog()
# d1 and d2 are two independent objects built from the same blueprint
__init__ and self#
__init__ runs automatically when you create an object and sets up its initial state. self is the object being set up, so self.name = name means "on this object, set name".
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
d1 = Dog("Bruno", 3)
d2 = Dog("Max", 5)
print(d1.name) # Bruno
print(d2.name) # Max
When you write Dog("Bruno"), Python effectively calls Dog.__init__(obj, "Bruno"), passing the new object in as self. You can prove that self and the variable are the same object:
class Dog:
def __init__(self, name):
self.name = name
print(id(self)) # memory address of the object being built
d1 = Dog("Bruno")
print(id(d1)) # same address as printed above
Instance variables vs class variables#
Instance variables are set on self (usually in __init__). Each object has its own copy.
Class variables are defined in the class body, outside any method. All objects share one copy.
class Dog:
species = "Canis familiaris" # class variable: shared
def __init__(self, name):
self.name = name # instance variable: per object
d1 = Dog("Bruno")
d2 = Dog("Max")
print(d1.species, d2.species) # Canis familiaris Canis familiaris
Dog.species = "Unknown" # changing it on the class affects everyone
print(d1.species, d2.species) # Unknown Unknown
__dict__: where attributes live#
Every class and object has a __dict__ that stores its attributes.
d1 = Dog("Bruno")
print(d1.__dict__) # {'name': 'Bruno'}
print(Dog.__dict__) # {'species': ..., '__init__': <function ...>, ...}
| What | Stored in |
|---|---|
| Instance variables | obj.__dict__ |
| Class variables | Class.__dict__ |
| Methods | Class.__dict__ |
Attribute lookup order#
When you read d1.species, Python searches in this order and stops at the first hit:
d1.__dict__(the instance)Dog.__dict__(the class)- Each parent class's
__dict__, and so on up the MRO
The shadowing trap#
Assigning a class variable through an object does not change the class. It creates a new instance variable that hides (shadows) the class one, for that object only.
class Dog:
species = "Canis familiaris"
def __init__(self, name):
self.name = name
d1 = Dog("Bruno")
d2 = Dog("Max")
d1.species = "Unknown"
print(d1.__dict__) # {'name': 'Bruno', 'species': 'Unknown'} <- new entry
print(d2.__dict__) # {'name': 'Max'} <- untouched
print(Dog.species) # Canis familiaris <- untouched
Reading the attribute back follows the lookup order:
| Read | Where Python finds it | Value |
|---|---|---|
d1.species |
d1.__dict__ |
"Unknown" |
d2.species |
Not in d2.__dict__, so Dog.__dict__ |
"Canis familiaris" |
| Class variable | Instance variable | |
|---|---|---|
| Defined | In class body, outside methods | On self, usually in __init__ |
| Shared? | Yes, by all objects | No, one per object |
| Stored in | Class.__dict__ |
obj.__dict__ |
| Assign via class | Affects all objects | n/a |
| Assign via object | Creates a shadowing instance variable | Affects only that object |
Class variables are shared, instance variables are owned. Assigning on an object never modifies the class variable; it creates a shadow. Lookup always goes instance, then class, then parents.
How an object is created: __new__ then __init__#
A common misconception is that __init__ is the constructor. It is the initializer. Creation is a two-step process:
When you write d1 = Dog("Bruno"):
- Python calls
__new__(Dog, "Bruno"), which creates an emptyDogobject and returns it. - Python calls
__init__(obj, "Bruno")on that object, which fillsobj.__dict__and returnsNone. - The new object is bound to
d1.
class Dog:
def __new__(cls, name):
# __new__ receives the same arguments as __init__, so accept them here
print("__new__: creating object")
return super().__new__(cls) # allocate an empty Dog
def __init__(self, name):
print("__init__: filling object")
self.name = name
d1 = Dog("Bruno")
# __new__: creating object
# __init__: filling object
__new__ |
__init__ |
|
|---|---|---|
| Role | Constructor | Initializer |
| Called | First | Second |
| Receives | cls (the class) |
self (the new object) |
| Job | Create and return the object | Set attributes on it |
| Returns | The new object | Always None |
| Override? | Rarely | Almost always |
When you need __new__: Singleton#
The main time you override __new__ is to control whether a new object is created at all, as in the Singleton pattern:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance # every call returns the same object
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True
Common __init__ patterns#
Default arguments:
class Dog:
def __init__(self, name, age=0):
self.name = name
self.age = age
Dog("Bruno") # age = 0
Dog("Max", 5) # age = 5
Calling the parent initializer with super():
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # let Animal set up its part
self.breed = breed
d1 = Dog("Bruno", "Labrador")
print(d1.name, d1.breed) # Bruno Labrador
Key takeaways#
- A class is a blueprint; an object is an instance with its own state.
- Instance variables live in
obj.__dict__; class variables and methods live inClass.__dict__. - Attribute lookup goes instance, then class, then parent classes.
- Assigning a class variable through an object creates a shadowing instance variable.
__new__creates the object,__init__initializes it and returnsNone. Override__new__mainly for patterns like Singleton.