You need Python installed, and to know how to run a
.py file. If python3 --version in a
terminal prints a version number, you are ready. If it does not — or
if you have never opened a terminal — do
Getting Started with
Python first; it installs Python and runs your first program, and
takes about fifteen minutes. Nothing else is needed: no account, no
payment, no extra software.
What is Object-Oriented Programming?
Object-Oriented Programming (OOP) organizes code around objects — bundles of data and behavior that model real-world things. A class is the blueprint; an object is an instance built from that blueprint.
Think of it like this: "Dog" is a class (the concept), while "Rex the golden retriever" is an object (a specific dog).
Creating Your First Class
class Dog:
def __init__(self, name, breed):
self.name = name # instance attribute
self.breed = breed # instance attribute
def bark(self): # method
print(f"{self.name} says: Woof!")
def info(self): # method
print(f"{self.name} is a {self.breed}")
# Create objects (instances)
rex = Dog("Rex", "Golden Retriever")
luna = Dog("Luna", "German Shepherd")
rex.bark() # Rex says: Woof!
luna.info() # Luna is a German Shepherd
self?
self refers to the specific object calling the method. When you
write rex.bark(), Python passes rex as self
automatically. Every method must have self as its first parameter.
The __init__ Constructor
__init__ runs automatically when you create a new object. Use it to
set up the object's initial state:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.transactions.append(f"+${amount}")
print(f"Deposited ${amount}. Balance: ${self.balance}")
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds!")
return
self.balance -= amount
self.transactions.append(f"-${amount}")
print(f"Withdrew ${amount}. Balance: ${self.balance}")
def statement(self):
print(f"\nAccount: {self.owner}")
print(f"Balance: ${self.balance}")
print(f"Transactions: {', '.join(self.transactions)}")
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500) # Deposited $500. Balance: $1500
account.withdraw(200) # Withdrew $200. Balance: $1300
account.statement()
Instance vs Class Attributes
class Car:
# Class attribute — shared by ALL instances
wheels = 4
def __init__(self, make, model, year):
# Instance attributes — unique to each object
self.make = make
self.model = model
self.year = year
self.mileage = 0
def drive(self, km):
self.mileage += km
car1 = Car("Toyota", "Camry", 2024)
car2 = Car("Honda", "Civic", 2023)
print(car1.wheels) # 4 (from class)
print(car2.wheels) # 4 (from class)
print(Car.wheels) # 4 (access via class)
car1.drive(100)
print(car1.mileage) # 100
print(car2.mileage) # 0 (each has its own)
Special Methods (Dunder Methods)
Python uses double-underscore methods (dunder methods) to define how objects behave with built-in operations:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
"""Called by print() and str()"""
return f"{self.name}: ${self.price:.2f}"
def __repr__(self):
"""Called in debugger and interactive shell"""
return f"Product('{self.name}', {self.price})"
def __eq__(self, other):
"""Called by == operator"""
return self.name == other.name and self.price == other.price
def __lt__(self, other):
"""Called by < operator (enables sorting)"""
return self.price < other.price
items = [
Product("Laptop", 999.99),
Product("Mouse", 29.99),
Product("Keyboard", 79.99)
]
print(items[0]) # Laptop: $999.99
items.sort() # Sorts by price (uses __lt__)
for item in items:
print(item) # Mouse, Keyboard, Laptop
Encapsulation
Encapsulation means hiding internal details and controlling access to data:
class User:
def __init__(self, username, password):
self.username = username
self._password = password # Convention: "private" (one underscore)
def check_password(self, attempt):
return attempt == self._password
def change_password(self, old, new):
if self.check_password(old):
self._password = new
print("Password changed!")
else:
print("Wrong password!")
user = User("alice", "secret123")
user.check_password("secret123") # True
user.change_password("secret123", "newpass456")
A single underscore (_name) signals "don't access this directly."
A double underscore (__name) triggers name mangling for stronger
protection. Neither truly prevents access — Python trusts developers.
Practical Example: Task Manager
class Task:
def __init__(self, title, priority="medium"):
self.title = title
self.priority = priority
self.completed = False
def complete(self):
self.completed = True
def __str__(self):
status = "done" if self.completed else "pending"
return f"[{status}] {self.title} ({self.priority})"
class TaskManager:
def __init__(self):
self.tasks = []
def add(self, title, priority="medium"):
task = Task(title, priority)
self.tasks.append(task)
print(f"Added: {title}")
def complete(self, title):
for task in self.tasks:
if task.title == title:
task.complete()
print(f"Completed: {title}")
return
print(f"Task not found: {title}")
def show(self, show_completed=True):
for task in self.tasks:
if show_completed or not task.completed:
print(f" {task}")
def pending_count(self):
return sum(1 for t in self.tasks if not t.completed)
# Usage
mgr = TaskManager()
mgr.add("Write report", "high")
mgr.add("Buy groceries", "low")
mgr.add("Fix bug #42", "high")
mgr.complete("Write report")
print(f"\nAll tasks:")
mgr.show()
print(f"\nPending: {mgr.pending_count()}")
Now Do It Yourself: Five Steps
A class is a template for making things that carry their own data and know how to do their own jobs. You will define one, make two of them, and meet the two errors every beginner hits. Every output below is exactly what Python printed.
Go: open a terminal, run cd ~, and create a file called dogs.py.
Do: type these six lines and run python3 dogs.py.
class Dog:
def __init__(self, name):
self.name = name
d = Dog("Rex")
print(d.name)
You should see: Rex. class Dog is the template; Dog("Rex") makes one actual dog. __init__ runs automatically at that moment and its job is to store what makes this dog different from any other.
If not: TypeError: Dog.__init__() missing 1 required positional argument: 'name' means you wrote Dog() with nothing inside. __init__ asks for a name, so you must supply one.
Go: same file, add to it.
Do: make the file read exactly this and run it.
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " says woof"
d = Dog("Rex")
print(d.speak())
You should see: Rex says woof. self is the particular dog the method was called on. You never pass it — writing d.speak() hands d in as self automatically. That is the whole trick.
If not: TypeError: Dog.speak() takes 0 positional arguments but 1 was given means you wrote def speak(): without self. The message reads backwards until you know what it means: Python passed the object in, and your method had nowhere to put it. Every method needs self as its first argument.
Go: same file, change only the bottom.
Do: replace the last two lines with these four and run it.
rex = Dog("Rex")
bella = Dog("Bella")
print(rex.speak())
print(bella.speak())
You should see: Rex says woof then Bella says woof. One class, two independent objects, each carrying its own name. Changing rex.name would not touch bella at all.
If not: if both print the same name, you assigned to Dog.name instead of self.name somewhere — that puts one value on the class itself and every object shares it. Anything that belongs to one object goes on self.
Go: same file, replace the contents.
Do: type these eight lines and run it.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def describe(self):
return f"{self.name} is {self.age} years old"
print(Dog("Rex", 3).describe())
You should see: Rex is 3 years old. Two pieces of data on one object, and a method that reads both. Note the object was never stored in a variable — it was made, used, and discarded on one line, which is perfectly normal.
If not: AttributeError: 'Dog' object has no attribute 'age' means __init__ accepted age but never stored it with self.age = age. Accepting an argument and keeping it are two separate acts.
Go: same file, replace the bottom line.
Do: use these four lines instead and run it.
rex = Dog("Rex", 3)
print(rex.describe())
rex.age = 4
print(rex.describe())
You should see: Rex is 3 years old then Rex is 4 years old. The object carries its data with it, and the method reads whatever is current at the moment it runs.
If not: if both lines say 3, you assigned to a copy rather than to rex — check you wrote rex.age = 4 and not age = 4. The second one just makes an unrelated variable and leaves the dog untouched.
Without scrolling up: you get TypeError: Dog.speak() takes 0 positional
arguments but 1 was given. What is wrong, and where? Answer:
speak was defined without self. Python always passes the
object as the first argument, so every method must accept it.
Now do it without the page: write a BankAccount class
holding an owner and a balance, with a
deposit(amount) method that increases the balance and returns the new
total. Same shape as step 4, plus one line that changes self.
Summary
- Classes are blueprints; objects are instances created from classes
__init__initializes new objects;selfrefers to the current instance- Instance attributes belong to each object; class attributes are shared
- Dunder methods (
__str__,__eq__, etc.) customize built-in behavior - Use underscore prefix (
_name) to signal private attributes - Classes bundle related data and behavior together, making code organized and reusable
You can now create your own classes and objects. Next up: inheritance and polymorphism — building class hierarchies and sharing code between related classes.