PYTHON / OBJECT-ORIENTED PYTHON
Classes, instances, and state
Create classes, build independent instances from them, and track each object's own state using vars(), is, and type().
What you will learn
- Call a class like a function to build a new, independent instance
- Inspect an instance's state as a plain dict with vars(obj) or obj.__dict__
- Use is and type() to tell object identity apart from matching attribute values
- Recognise that b = a aliases one object instead of copying its state
Understanding Classes, instances, and state
A class statement does not create data; it creates one object, the class itself, which acts both as a type and as a factory. When you write Robot(), Python allocates a brand new empty object, tags it with Robot as its type, and hands it back to you. Every call produces a separate object, so ten calls give ten unrelated things that merely share the same type. That split is the whole point: the class is written once and describes a kind of thing, while each instance carries the particular values for one of those things.
The state of an instance is a per-object namespace, implemented as an ordinary dictionary you can look at with vars(obj) or obj.__dict__. Writing robot.charge = 100 inserts a key into that dictionary, and reading robot.charge looks it up there first before falling back to the class. Nothing in the class body declares which fields exist, so an instance's shape is exactly whatever has been assigned to it so far, and it can change at any moment. Because state lives in the object rather than in the class, changing one instance leaves every other instance untouched.
Identity matters as soon as more than one name points at your objects. A name is just a label bound to an object, so alias = robot creates a second label for the same dictionary of state, and a change made through either name is visible through both. Use is (or id()) to ask "are these the same object?" and type() to ask "were these built from the same class?" — they are different questions. Note also that == on a plain instance falls back to identity, so two objects created the same way with identical attributes still compare as unequal.
class Robot:
"""A machine with a name and a charge level."""
robot_a = Robot()
robot_a.name = "R2"
robot_a.charge = 100
robot_b = Robot()
robot_b.name = "C3"
robot_b.charge = 100
robot_a.charge -= 35
print(robot_a.name, robot_a.charge)
print(robot_b.name, robot_b.charge)
print("same object:", robot_a is robot_b)
print("same class:", type(robot_a) is type(robot_b) is Robot)
print("state of a:", vars(robot_a))
print("state of b:", vars(robot_b))A class is a type and a factory; each call to it returns a distinct object whose state lives in that object's own namespace.
Worked examples
Two names, one object
Shows that assigning an instance to a second name aliases it instead of copying its state.
class Cart:
"""Holds items a shopper has selected."""
first = Cart()
first.items = []
first.items.append("lamp")
alias = first
alias.items.append("rug")
second = Cart()
second.items = []
second.items.append("desk")
print(first.items)
print(alias is first, id(alias) == id(first))
print(second.items)Example explained
Line 1alias = first binds a second name to the existing object; no new instance is made.
Line 2alias.items.append("rug") mutates the one list stored on that object, so first.items shows both items.
Line 3alias is first and id(alias) == id(first) both report True because there is only one object.
Line 4second = Cart() is a real second instance, so its items list is completely separate.
Equal-looking instances are not equal
Demonstrates that == on a plain instance compares identity, not attribute values.
class Point:
"""A location on a grid."""
p = Point()
p.x, p.y = 1, 2
q = Point()
q.x, q.y = 1, 2
print(vars(p) == vars(q))
print(p == q)
print(p == p)
print(len({p, q}))Example explained
Line 1vars(p) == vars(q) is True: the two state dictionaries really do hold the same values.
Line 2p == q is still False, because a class with no __eq__ compares objects by identity.
Line 3p == p is True only because it is literally the same object.
Line 4The set has 2 members: default hashing also follows identity, so the objects never collide.
State is created by assignment, not declared
Shows that one instance can carry an attribute a sibling instance simply does not have.
class Session:
"""Tracks one visitor."""
live = Session()
live.user = "ada"
live.page_views = 3
fresh = Session()
fresh.user = "grace"
print(hasattr(live, "page_views"), hasattr(fresh, "page_views"))
print(vars(fresh))
try:
print(fresh.page_views)
except AttributeError as err:
print("AttributeError:", err)Example explained
Line 1page_views exists only on live because that is the only object it was assigned to.
Line 2vars(fresh) proves the second instance's namespace holds a single key.
Line 3Reading fresh.page_views fails: attribute lookup finds nothing on the instance and nothing on the class.
Line 4This is why real classes set every field up front rather than adding attributes ad hoc.
Important notes
vars(obj) and obj.__dict__ are useful for inspection and debugging, but classes that define __slots__ have no such dictionary, so do not build logic around it.
Assigning attributes from outside the class works but is fragile; the usual practice is to set all state when the object is created and change it through methods.
Common mistakes
Writing a = Thing instead of a = Thing(): a now names the class itself, so a.size = 3 writes onto the class and every later instance sees that value.
Assuming b = a makes a copy, then mutating b.items and being surprised that a.items changed too, because both names point at one object.
Expecting two instances built with identical attribute values to compare equal with ==; without __eq__ the comparison is identity, so the test silently fails.
Try it yourself
Change, predict, then run
Define a class Lamp, create two instances, give one brightness 5 and the other brightness 1, then raise only the first one's brightness by 3 and print vars() for both to prove the second was untouched.
Open the Python workspaceCheck your understanding
class Box: pass; a = Box(); a.size = 1; b = a; c = Box(); c.size = 1; b.size = 9; print(a.size, c.size, a == c) — what is printed?
- 9 1 False
- 1 1 False
- 9 9 True
- 9 1 True
Show answer
b = a binds a second name to the same object, so b.size = 9 is visible as a.size, while c is a separate instance whose size stays 1. The tempting '9 1 True' assumes == compares attribute values, but Box defines no __eq__, so == falls back to identity and a == c is False.