Module 14 — Object-Oriented Programming
Modeling real things as classes and objects — the backbone of large Python programs.
Prerequisite: Module 13 — Generators
After this lesson, you will be able to
- Define a class with an __init__ constructor and instance methods
- Explain the difference between instance variables and class variables
- Use inheritance to share behavior between related classes
- Use @property and basic magic methods like __str__
Concept & Syntax
Object-oriented programming (OOP) organizes code around objects — bundles of data
(attributes) and behavior (methods) modeled on real-world things. A class is the blueprint; an
object (or instance) is one specific thing built from that blueprint. Every Dog
object shares the same blueprint, but each has its own name, age, and so on.
self is simply how a method refers to "the particular object I was called on." It's always the
first parameter of an instance method, and Python passes it automatically — you never supply it yourself when
calling the method.
class ClassName:
def __init__(self, attribute):
self.attribute = attribute
def method(self):
return self.attribute
obj = ClassName("value")Examples
Your first class Very Easy
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return self.name + " says Woof!"
rex = Dog("Rex")
print(rex.bark())__init__ runs automatically when Dog("Rex") is called, storing "Rex" as this object's name attribute.
Multiple objects, independent state Easy
rex = Dog("Rex")
fido = Dog("Fido")
print(rex.bark())
print(fido.bark())Each object keeps its own separate copy of name — that's what 'instance' variables means.
Class variables vs instance variables Intermediate
class Dog:
species = "Canis familiaris" # class variable — shared by all dogs
def __init__(self, name):
self.name = name # instance variable — unique per dog
rex = Dog("Rex")
fido = Dog("Fido")
print(rex.species, fido.species)
print(rex.name, fido.name)species is defined once on the class and shared by every instance; name is set per-object inside __init__.
Inheritance Real World
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} barks."
rex = Dog("Rex")
print(rex.speak())class Dog(Animal) means Dog inherits everything from Animal, then overrides speak() with its own version — this is called method overriding.
@property and __str__ Challenge
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
def __str__(self):
return f"Circle(radius={self.radius})"
c = Circle(4)
print(c)
print(c.area)@property lets you call c.area like a plain attribute even though it's computed. __str__ controls what print() shows for an object.
Visual Explanation
🔍 What happens internally? (advanced)
Under the hood, Python compiles your source into bytecode and runs it on a virtual machine (the CPython interpreter). Variables are names in a namespace dictionary pointing at objects in memory — which is exactly what the memory panel in the tracer above is showing you.
Watch an object get created and used
Use the controls below to run this example one line at a time and watch the interpreter's memory update live.
Code
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return self.name + " says Woof!"
rex = Dog("Rex")
print(rex.bark())
Interpreter state
Console output
Common Mistakes
Forgetting self as the first parameter of a method is one of the most common OOP errors for beginners.
class Dog:
def bark():
return "Woof!"
rex = Dog()
rex.bark()class Dog:
def bark(self):
return "Woof!"
rex = Dog()
rex.bark()Confusing a class variable with an instance variable can cause values to be unexpectedly shared across every object.
class Cart:
items = [] # class variable — shared by ALL carts!
def add(self, item):
self.items.append(item)class Cart:
def __init__(self):
self.items = [] # instance variable — one per cart
def add(self, item):
self.items.append(item)- Name classes in CapitalizedWords (PascalCase): Dog, ShoppingCart, not dog or shopping_cart.
- Set instance-specific data inside __init__ using self, not as a bare class-level variable.
- Favor composition or simple inheritance chains over deep, multi-level inheritance hierarchies.
- Add __str__ to classes you plan to print, so debugging output is readable.
Exercise & Challenge
Exercise 1 — Predict the output
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
whiskers = Cat("Whiskers", 3)
print(whiskers.age)What does this print?
🏆 Challenge
Model a BankAccount class with a balance, a deposit(amount) method, and a withdraw(amount) method that refuses to overdraw the account.
- Store balance as an instance variable, starting at 0
- deposit() should increase balance
- withdraw() should print an error message instead of allowing a negative balance
💡 Need a hint?
Check `if amount > self.balance:` before subtracting inside withdraw().
🔍 Show a sample solution
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds")
else:
self.balance -= amount
acc = BankAccount()
acc.deposit(100)
acc.withdraw(30)
print(acc.balance)Quiz
Answer every question, then submit to see your score and explanations.
1. What is self inside an instance method?
2. What is the difference between a class variable and an instance variable?
3. What does class Dog(Animal) mean?
4. What does __init__ do?
5. What does @property allow you to do?
Summary
- A class is a blueprint; an object is one instance built from it.
- self refers to the current object inside an instance method.
- Instance variables are per-object; class variables are shared by all instances.
- Inheritance (class Dog(Animal)) lets a subclass reuse and override a superclass's behavior.