You already have objects
Run this before we go any further:
1x = 422print(type(x))34name = "alice"5print(type(name))67items = [1, 2, 3]8print(type(items))
You wrote three lines of Python you already know. The output is telling you something most people miss until much later: every value belongs to a class. 42 is an instance of int. "alice" is an instance of str. This is not a detail. It is the entire design philosophy of Python.
Objects carry their behavior with them
In languages like C, a string is just a block of memory. You pass it to separate functions to do anything with it. strlen(s), toupper(s). The data and the functions that operate on it are separate things.
Python strings carry their operations with them:
1name = "alice"23print(name.upper()) # ALICE4print(name.capitalize()) # Alice5print(name.replace("a", "@")) # @lice6print(name.startswith("a")) # True
Those methods are not standalone functions floating somewhere. They live on the string object. Every string object has them. That is what makes something an object: it bundles data and the operations that work on that data into one thing.
dir() shows you everything an object knows how to do
1x = 422print(dir(x))
A plain integer knows 77 things about itself. The ones with double underscores (__add__, __mul__) are what Python calls when you write x + y or x * y. There is a whole article on those later in this series. For now, the point is: integers are not simple numbers. They are objects with dozens of built-in behaviors.
Even functions are objects:
1def greet(name):2 return f"Hello, {name}"34print(type(greet))5print(greet.__name__)6print(greet.__doc__)
Everything. Functions, classes themselves, modules. All objects. This is why Python can do things like pass a function as an argument to another function, store a function in a list, or return a function from a function. Objects can go anywhere.
State and behavior
Every object has two things: state (data it holds) and behavior (things it can do). A string's state is the characters it contains. Its behavior is methods like upper() and split(). A list's state is the items it holds. Its behavior is append(), sort(), and so on.
You can inspect state and call behavior on any object:
1# A list object2scores = [88, 72, 95, 61, 90]34# Its state: the items it holds5print(scores) # [88, 72, 95, 61, 90]67# Its behavior: what it can do8scores.append(77)9scores.sort()10print(scores) # [61, 72, 77, 88, 90, 95]11print(len(scores)) # 61213# You can also ask it about itself14print(type(scores)) # <class 'list'>15print(id(scores)) # memory address, unique per object
Rule: state is the data an object holds. Behavior is what you can ask it to do. Every object has both.
Two objects of the same class, independent state
Here is something that trips beginners up. Two lists are both instances of list, but they do not share data. Each has its own independent state.
1a = [1, 2, 3]2b = [1, 2, 3]34# Same class, same initial values5print(type(a) == type(b)) # True6print(a == b) # True, same contents78# But completely independent objects9a.append(99)10print(a) # [1, 2, 3, 99]11print(b) # [1, 2, 3] -- b is untouched1213# Different locations in memory14print(id(a) == id(b)) # False
This is the key property of objects. The class is one thing, a shared description. Every instance you create from it gets its own private copy of state. Changing one does not affect the other.
When you define your own classes later, your objects work the same way. One class definition, unlimited independent instances.
The problem classes solve
Before showing you how to write a class, it helps to feel the pain of not having one.
Imagine you are building a bank account system. Without classes, you might reach for dictionaries to hold the data and separate functions to act on them:
1# No classes, just dicts and functions2def create_account(owner, balance=0):3 return {"owner": owner, "balance": balance}45def deposit(account, amount):6 account["balance"] += amount78def withdraw(account, amount):9 if amount > account["balance"]:10 raise ValueError("insufficient funds")11 account["balance"] -= amount1213def get_balance(account):14 return account["balance"]1516alice = create_account("Alice", 100)17bob = create_account("Bob", 50)1819deposit(alice, 200)20withdraw(bob, 30)2122print(get_balance(alice)) # 30023print(get_balance(bob)) # 20
This works. But notice what you are doing manually every time: passing the account dict as the first argument to every function. The data and the functions that operate on it are separate. Nothing stops you from calling deposit(bob, -500) or from reaching directly into the dict with alice["balance"] = 999999 and bypassing the validation entirely.
As the codebase grows, more functions accumulate. Some use account["balance"], some use account.get("balance", 0). Nobody can tell which functions belong to accounts and which belong to users. The relationship between data and operations is invisible.
A class groups them
A class does one thing: it bundles state (the data) and behavior (the functions that operate on that data) into a single named unit. Here is the same bank account as a class:
1class BankAccount:2 def __init__(self, owner, balance=0):3 self.owner = owner4 self.balance = balance56 def deposit(self, amount):7 self.balance += amount89 def withdraw(self, amount):10 if amount > self.balance:11 raise ValueError("insufficient funds")12 self.balance -= amount1314alice = BankAccount("Alice", 100)15bob = BankAccount("Bob", 50)1617alice.deposit(200)18bob.withdraw(30)1920print(alice.balance) # 30021print(bob.balance) # 20
The functions are no longer floating separately. They live on the object. When you call alice.deposit(200), Python automatically passes alice as the first argument. You do not have to write deposit(alice, 200) every time. The object knows which data it belongs to.
A class is not magic. It is a way of organizing code so that the data and the operations that belong together stay together. Everything else in OOP is built on top of that idea.
The next article walks through writing your first class from scratch, piece by piece. But now you understand why classes exist. Not because Python requires them. Because keeping data and behavior together makes code easier to understand, easier to extend, and harder to break accidentally.