Classes & objects

You know OOP from Java — but Python's object model is more transparent and a little more surprising. Attributes live in plain dictionaries, the class is itself an object, and lookup follows one simple rule: instance first, then class. Get that rule and class-vs-instance variables, self, and @property all click.

selfinstance vs class vars attribute lookupclassmethod / staticmethod @property

Two namespaces, one lookup rule

Every instance carries its own little dictionary of attributes (its __dict__). The class has a separate namespace holding things shared by all instances — methods, and any class variables. When you read obj.x, Python looks in the instance first; only if it's not there does it fall back to the class. That single rule explains a lot.

An instance variable (set with self.x = …, usually in __init__) is unique to each object. A class variable (declared in the class body) is shared by all of them — until an instance assigns to that name, which creates an instance variable that shadows the class one for that object only. Try it: read attributes (watch the lookup path) and assign them (watch where the value lands).

Class Account has a shared class var bank; each instance gets its own owner & balance.

Methods come in three flavours

What about the functions in the class body? They differ only by what automatic first argument they get:

class Account:
  bank = "PyBank"  # class variable (shared)
  def __init__(self, owner, balance):  # self = this instance
    self.owner = owner; self.balance = balance
  def deposit(self, x):    # instance method → gets the object
    self.balance += x
  @classmethod
  def from_dict(cls, d):  # classmethod → gets the class
    return cls(d["owner"], d["balance"])
  @staticmethod
  def is_valid(x):    # staticmethod → gets nothing special
    return x > 0

@property: a method that looks like an attribute

Java reflexively wraps fields in getBalance()/setBalance(). Python says: just expose the attribute — and if you later need logic on access, add it without changing the callers, using @property:

@property
def is_overdrawn(self):
  return self.balance < 0

a1.is_overdrawn  # accessed with NO parentheses — runs the method

It runs a method but is read like a field — no (). That keeps your public surface clean while still letting you compute, validate, or cache behind it.

Takeaways: attributes live in dicts; lookup is instance → class. self.x makes a per-object instance var; a name in the class body is a shared class var (assigning it on an instance shadows, never mutates the shared one). Methods differ by their auto first arg: self (instance), cls (@classmethod), nothing (@staticmethod). @property exposes a method as a parenthesis-free attribute.

See instances and their __dict__s as real boxes in Python Tutor. Next: inheritance & the MRO →