Instance Variables and Methods in Python: Where Object State Lives
Every object in Python needs somewhere to keep its data. That somewhere is instance variables. Every object also needs behaviour — ways to act on or report its data. That behaviour comes from instance methods. Together, they define what an object is and what it can do.
Instance Variables: Data That Belongs to One Object
An instance variable is an attribute attached to a specific object. Two objects of the same class can hold completely different values for the same attribute name, and neither affects the other.
Instance variables are typically set in __init__() using the self prefix:
class Sensor: def __init__(self, name, unit): self.name = name # instance variable self.unit = unit # instance variable self.readings = [] # instance variable — starts empty for each sensor
temp_sensor = Sensor("Temperature", "°C")pressure_sensor = Sensor("Pressure", "hPa")
temp_sensor.readings.append(22.3)print(temp_sensor.readings) # [22.3]print(pressure_sensor.readings) # [] — completely independent listEach Sensor object gets its own readings list. Appending to temp_sensor.readings has no effect on pressure_sensor.
Declaring Instance Variables Outside __init__()
Python allows adding instance variables after object creation, but this is usually a mistake. Code that reads obj.x without knowing whether x was set is fragile.
class Player: def __init__(self, username): self.username = username self.score = 0
p = Player("alice")p.level = 5 # added after creation — works, but not recommended
print(p.__dict__) # {'username': 'alice', 'score': 0, 'level': 5}The __dict__ attribute holds a dictionary of all instance variables. It is useful for debugging and serialisation but avoid relying on it in production logic.
The consistent pattern is: declare all instance variables in __init__(), even if they start as None or an empty collection. This makes the class’s interface explicit at a glance.
Instance Methods: Behaviour That Operates on Object State
An instance method is a function defined inside a class that automatically receives self as its first argument. Through self, the method can read and modify the object’s instance variables.
class BankAccount: def __init__(self, owner, initial_balance=0): self.owner = owner self._balance = initial_balance # underscore signals "handle carefully" self._transactions = []
def deposit(self, amount): if amount <= 0: raise ValueError("Deposit amount must be positive") self._balance += amount self._transactions.append(("deposit", amount))
def withdraw(self, amount): if amount > self._balance: raise ValueError("Insufficient funds") self._balance -= amount self._transactions.append(("withdrawal", amount))
def statement(self): lines = [f"Account: {self.owner}", f"Balance: £{self._balance:.2f}", "---"] for kind, amount in self._transactions: lines.append(f" {kind.capitalize()}: £{amount:.2f}") return "\n".join(lines)
account = BankAccount("Morgan", 500)account.deposit(200)account.withdraw(75)print(account.statement())Output:
Account: MorganBalance: £625.00--- Deposit: £200.00 Withdrawal: £75.00The methods handle all the rules. Code that holds a BankAccount object calls deposit() and withdraw() without needing to know how the balance is stored or how transactions are tracked.
How self Works
self is not magic — it is a regular parameter. When you call account.deposit(200), Python translates that into BankAccount.deposit(account, 200). The object is passed as the first argument automatically.
class Greeter: def __init__(self, name): self.name = name
def greet(self): return f"Hello, I'm {self.name}"
g = Greeter("Sam")
# These two calls are identicalprint(g.greet()) # Hello, I'm Samprint(Greeter.greet(g)) # Hello, I'm SamBoth forms call the same code. Python’s method call syntax is syntactic sugar for passing the object as the first argument.
Instance Variables vs Class Variables
Knowing the difference prevents one of the most common OOP bugs in Python.
class Robot: population = 0 # class variable — shared across ALL instances
def __init__(self, model): self.model = model # instance variable — unique per robot Robot.population += 1 # increment the shared counter
def __del__(self): Robot.population -= 1
r1 = Robot("R2D2")r2 = Robot("C3PO")
print(Robot.population) # 2print(r1.model) # R2D2print(r2.model) # C3POAccessing r1.population reads the class variable (Python looks up the chain when it does not find it on the instance). But assigning r1.population = 10 creates a new instance variable named population on r1, hiding the class variable — the class variable stays at 2. Always modify class variables through the class name: Robot.population.
Introspection: Examining Object State
Python offers several ways to inspect what an object holds at runtime.
class Article: def __init__(self, title, author, word_count): self.title = title self.author = author self.word_count = word_count
a = Article("Deep Work", "Cal Newport", 68000)
# Dict of instance variablesprint(a.__dict__)# {'title': 'Deep Work', 'author': 'Cal Newport', 'word_count': 68000}
# Check if an attribute existsprint(hasattr(a, "title")) # Trueprint(hasattr(a, "isbn")) # False
# Get attribute by name (useful when attribute name is a string)print(getattr(a, "author")) # Cal Newportprint(getattr(a, "isbn", "N/A")) # N/A — default if not foundgetattr with a default is particularly useful in serialisation code where you cannot guarantee every attribute is present.
Common Mistakes
Accessing self outside a method. The self reference only exists inside instance methods. You cannot use it in the class body directly (only in method definitions).
Shadowing instance variables with local variables. If you name a local variable the same as an instance variable inside a method, you may accidentally read the local one instead of self.x.
class Counter: def __init__(self): self.count = 0
def increment(self, count=1): # parameter named 'count' shadows nothing here, self.count += count # but be careful with names
def reset(self): count = 0 # this is a local variable # self.count is unchanged! self.count = 0 # this is what you meantPrevious > The __init__() Constructor Next > Class Variables and @classmethod