Python Classes Objects
A class is a blueprint for creating objects — it bundles together the data (called attributes) and the behavior (called methods) that describe a category of things. An object, also called an instance, is a specific, concrete thing built from that blueprint, with its own independent copy of the data. Classes are the foundation of object-oriented programming (OOP) in Python, letting you model real entities — a Dog, a BankAccount, an Employee — as self-contained units that carry their own state and know how to act on it. Understanding classes deeply matters because almost everything in Python, including strings, lists, and exceptions, is itself an object created from a class.
Overview: How Classes and Objects Work
When you write class Dog:, Python does not create any dogs. It creates a new type object named Dog and stores it in the current namespace, the same way a def statement creates a function object. This type object has its own namespace (its __dict__) that holds everything defined in the class body: methods, and any class attributes (variables assigned directly inside the class body, outside of any method).
Calling Dog("Buddy", 3) is what actually produces an object. Python allocates a brand-new, empty instance, then calls the class’s __init__ method to initialize it, passing the new instance in automatically as the first argument, conventionally named self. Whatever you assign to self.something inside __init__ becomes an instance attribute, stored in that specific object’s own __dict__ — completely separate from any other instance’s dictionary.
This distinction between class attributes and instance attributes is the single most important thing to internalize. A class attribute lives once, on the class, and is shared by every instance. An instance attribute lives separately on each object. When you write buddy.name, Python looks first in buddy.__dict__; if it is not found there, it falls back to looking in Dog.__dict__ (and then up the inheritance chain). This is why methods — which live only in the class’s dictionary — are accessible from every instance without being copied into each one, while data you set with self.x = ... stays private to that object.
Methods themselves are just functions defined inside the class body. What makes instance.method() work without you manually passing the instance is the descriptor protocol: functions implement a special __get__ method, so when you access buddy.bark, Python doesn’t hand you the raw function — it hands you a bound method, a small wrapper that remembers buddy and will automatically supply it as the first argument (self) when you call it. That’s the entire mechanism behind “methods know which object they belong to.”
Syntax
class ClassName:
class_attribute = value # shared by every instance
def __init__(self, param1, param2):
self.attr1 = param1 # instance attribute
self.attr2 = param2 # instance attribute
def method_name(self, arg):
return self.attr1 + arg # can read/write instance state
instance = ClassName(value1, value2) # creates and initializes an object
instance.method_name(arg) # calls a bound method
| Part | Meaning |
|---|---|
class ClassName: |
Defines a new type; convention is CapWords naming. |
class_attribute |
Assigned directly in the class body; one copy shared by all instances. |
__init__ |
The initializer, automatically called right after an instance is created. |
self |
The instance itself; always the first parameter of an instance method. |
self.attr1 = ... |
Creates or updates an attribute on this specific instance. |
ClassName(...) |
Instantiation: creates a new object and runs __init__ on it. |
Examples
Example 1: A basic class with instance and class attributes
class Dog:
species = "Canis familiaris"
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def bark(self) -> str:
return f"{self.name} says Woof!"
def __str__(self) -> str:
return f"{self.name} ({self.age} years old)"
buddy = Dog("Buddy", 3)
miles = Dog("Miles", 5)
print(buddy.bark())
print(miles)
print(buddy.species, miles.species)
Output:
Buddy says Woof!
Miles (5 years old)
Canis familiaris Canis familiaris
Each Dog instance gets its own name and age, but both share the single species value that lives on the class itself. print(miles) implicitly calls str(miles), which Python routes to our __str__ method.
Example 2: A class that manages mutable state
class BankAccount:
def __init__(self, owner: str, balance: float = 0.0):
self.owner = owner
self.balance = balance
self.transaction_log: list[str] = []
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
self.transaction_log.append(f"Deposited {amount:.2f}")
def withdraw(self, amount: float) -> None:
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
self.transaction_log.append(f"Withdrew {amount:.2f}")
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Grace", 100.0)
account.deposit(50)
account.withdraw(30)
print(account)
for entry in account.transaction_log:
print(entry)
Output:
BankAccount(owner='Grace', balance=120.00)
Deposited 50.00
Withdrew 30.00
Notice how each method mutates self rather than returning a new object — this is typical for objects that represent evolving state. Because we didn’t define __str__, print(account) falls back to __repr__.
Example 3: Class attributes for shared, class-wide bookkeeping
class Employee:
company = "Acme Corp"
_count = 0
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary
Employee._count += 1
def give_raise(self, percent: float) -> None:
self.salary *= 1 + percent / 100
@classmethod
def total_employees(cls) -> int:
return cls._count
alice = Employee("Alice", 70000)
bob = Employee("Bob", 65000)
alice.give_raise(10)
print(f"{alice.name}: {alice.salary:.2f}")
print(f"{bob.name}: {bob.salary:.2f}")
print(f"Total employees: {Employee.total_employees()}")
print(f"Company: {alice.company}, {bob.company}")
Output:
Alice: 77000.00
Bob: 65000.00
Total employees: 2
Company: Acme Corp, Acme Corp
Here _count is deliberately updated through the class (Employee._count), not through self, so every instantiation increments one shared counter instead of accidentally creating a separate instance attribute.
Under the Hood: What Happens Step by Step
- Python evaluates the
classbody once, top to bottom, storing every name it defines (methods, class attributes) into a fresh namespace dictionary. - Calling
Employee("Alice", 70000)first invokesEmployee.__new__(Employee), which allocates a blank instance with an empty__dict__. - Python then calls
Employee.__init__(instance, "Alice", 70000). Everyself.x = ...line inserts a key into that instance’s own__dict__. - When you later access
alice.company, Python checksalice.__dict__first (not found), then checkstype(alice).__dict__— found, returns"Acme Corp". - When you access
alice.give_raise, Python finds the function in the class dictionary and, thanks to the descriptor protocol, wraps it into a bound method tied toalicebefore returning it — that’s whyalice.give_raise(10)works without manually passingaliceas an argument.
Common Mistakes
Mistake 1: Forgetting the self parameter
A very common beginner error is defining a method without a first parameter to receive the instance:
class Dog:
def bark():
return "Woof!"
d = Dog()
d.bark() # TypeError: bark() takes 0 positional arguments but 1 was given
Python always passes the instance as the first argument to an instance method, whether you declared a parameter for it or not — so calling d.bark() actually tries to call bark(d), which fails because bark was defined to take zero arguments. The fix is to always accept self:
class Dog:
def bark(self):
return "Woof!"
d = Dog()
print(d.bark())
Output:
Woof!
Mistake 2: Using a mutable default argument
Default argument values are evaluated exactly once, when the function is defined — not each time it’s called. Using a mutable object like a list as a default creates one shared list reused by every instance that doesn’t pass its own:
class ShoppingCart:
def __init__(self, items=[]):
self.items = items
cart_a = ShoppingCart()
cart_b = ShoppingCart()
cart_a.items.append("apple")
print(cart_b.items) # ['apple'] -- unexpectedly shared between instances!
Because both cart_a and cart_b received the very same list object, appending to one silently affects the other. The fix is to default to None and create a fresh list inside __init__:
class ShoppingCart:
def __init__(self, items: list | None = None):
self.items = items if items is not None else []
cart_a = ShoppingCart()
cart_b = ShoppingCart()
cart_a.items.append("apple")
print(cart_a.items)
print(cart_b.items)
Output:
['apple']
[]
Best Practices
- Always name the first parameter of an instance method
self— it’s a universal convention that every Python reader relies on. - Initialize every instance attribute inside
__init__so all instances of a class have a consistent, predictable shape. - Use type hints on
__init__parameters and attributes to make the expected data clear to readers and editors. - Reserve class attributes for values that are genuinely constant and shared (like configuration or lookup tables); use instance attributes for anything that varies per object.
- Never use a mutable object (list, dict, set) as a default argument value — default to
Noneand build the mutable object inside the method body. - Implement
__repr__on classes you’ll debug with, soprint()and the interactive shell give you a useful, unambiguous representation. - Use
@classmethodfor alternate constructors and@staticmethodfor helper logic that doesn’t needselforclsat all. - Keep each class focused on a single responsibility; if a class is juggling many unrelated concerns, split it.
Practice Exercises
- Exercise 1: Write a
Rectangleclass whose__init__takeswidthandheight, with methodsarea()andperimeter(). Create two rectangles and print both values for each. - Exercise 2: Write a
Counterclass that uses a class attribute to track how many instances have been created in total, exposed through a@classmethodcalledtotal_created(). Create five instances and print the total. - Exercise 3: Take the buggy
ShoppingCartexample from the Common Mistakes section and rewrite it so that three separately created carts can each add items without affecting one another. Prove it works by printing all three carts’ items after modifying only one.
Summary
- A class is a blueprint; an object (instance) is a concrete thing built from that blueprint via a call like
ClassName(...). __init__runs automatically right after an instance is created and is where you set up instance attributes viaself.attr = value.- Instance attributes live in each object’s own
__dict__; class attributes live once on the class and are shared by all instances. - Attribute lookup checks the instance’s dictionary first, then falls back to the class’s dictionary.
- Methods are ordinary functions stored on the class; accessing them through an instance produces a bound method thanks to the descriptor protocol, which is why
selfis supplied automatically. - Avoid mutable default arguments and always give
selfas the first parameter of instance methods to sidestep the two most common class-related bugs.
