Codetail

Article 1 of 8

Objects Everywhere

You've been using OOP since line one.

15 min read

You already have objects

Run this before we go any further:

Python
1x = 42
2print(type(x))
3
4name = "alice"
5print(type(name))
6
7items = [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:

Python
1name = "alice"
2
3print(name.upper()) # ALICE
4print(name.capitalize()) # Alice
5print(name.replace("a", "@")) # @lice
6print(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

Python
1x = 42
2print(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:

Python
1def greet(name):
2 return f"Hello, {name}"
3
4print(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:

Python
1# A list object
2scores = [88, 72, 95, 61, 90]
3
4# Its state: the items it holds
5print(scores) # [88, 72, 95, 61, 90]
6
7# Its behavior: what it can do
8scores.append(77)
9scores.sort()
10print(scores) # [61, 72, 77, 88, 90, 95]
11print(len(scores)) # 6
12
13# You can also ask it about itself
14print(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.

Python
1a = [1, 2, 3]
2b = [1, 2, 3]
3
4# Same class, same initial values
5print(type(a) == type(b)) # True
6print(a == b) # True, same contents
7
8# But completely independent objects
9a.append(99)
10print(a) # [1, 2, 3, 99]
11print(b) # [1, 2, 3] -- b is untouched
12
13# Different locations in memory
14print(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:

Python
1# No classes, just dicts and functions
2def create_account(owner, balance=0):
3 return {"owner": owner, "balance": balance}
4
5def deposit(account, amount):
6 account["balance"] += amount
7
8def withdraw(account, amount):
9 if amount > account["balance"]:
10 raise ValueError("insufficient funds")
11 account["balance"] -= amount
12
13def get_balance(account):
14 return account["balance"]
15
16alice = create_account("Alice", 100)
17bob = create_account("Bob", 50)
18
19deposit(alice, 200)
20withdraw(bob, 30)
21
22print(get_balance(alice)) # 300
23print(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:

Python
1class BankAccount:
2 def __init__(self, owner, balance=0):
3 self.owner = owner
4 self.balance = balance
5
6 def deposit(self, amount):
7 self.balance += amount
8
9 def withdraw(self, amount):
10 if amount > self.balance:
11 raise ValueError("insufficient funds")
12 self.balance -= amount
13
14alice = BankAccount("Alice", 100)
15bob = BankAccount("Bob", 50)
16
17alice.deposit(200)
18bob.withdraw(30)
19
20print(alice.balance) # 300
21print(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.