Object Oriented Programming
Classes & Objects
A class is a blueprint for creating objects — it defines what data (attributes) and behavior (methods) every object of that type will have. An object (or instan
Jr Codex Python Notes
Level: Intermediate Prerequisites: Module 3: Modules, Data & Errors Time to complete: ~30 minutes
Table of Contents
- What is a Class? What is an Object?
- Defining a Class
__init__— The Constructor- Understanding
self - Instance Attributes vs Class Attributes
- Instance Methods
- Class Methods & Static Methods
- Summary & Next Steps
1. What is a Class? What is an Object?
A class is a blueprint for creating objects — it defines what data (attributes) and behavior (methods) every object of that type will have. An object (or instance) is one concrete thing built from that blueprint.
Class vs Object
─────────────────────────────────────────
Class: "Dog" (the blueprint)
- has a name, breed, age
- can bark(), eat(), sleep()
Objects: my_dog = Dog("Rex", "Labrador", 3) ← one specific dog
your_dog = Dog("Bella", "Poodle", 5) ← a DIFFERENT specific dog
(same blueprint, different data)
─────────────────────────────────────────
You've actually been using objects since Chapter 2 of Module 1 — every string, list, and dict in Python is an object, created from the built-in str, list, and dict classes:
my_list = [1, 2, 3]
print(type(my_list)) # <class 'list'>
my_list.append(4) # .append() is a METHOD defined on the list classThis chapter shows you how to define your own classes, the same way list and dict are defined.
2. Defining a Class
class Dog:
pass # empty class — valid, though not useful yet
my_dog = Dog() # create an INSTANCE of Dog
print(type(my_dog)) # <class '__main__.Dog'>Naming convention (PEP 8): class names use PascalCase (also called CapWords), unlike variables and functions which use snake_case:
class UserAccount: # ✓ PascalCase for classes
pass
user_account = UserAccount() # ✓ snake_case for the instance/variable3. __init__ — The Constructor
__init__ is a special method that runs automatically when a new object is created — it's where you set up the object's initial data.
class Dog:
def __init__(self, name, breed, age):
self.name = name
self.breed = breed
self.age = age
my_dog = Dog("Rex", "Labrador", 3)
print(my_dog.name) # "Rex"
print(my_dog.breed) # "Labrador"
print(my_dog.age) # 3
your_dog = Dog("Bella", "Poodle", 5) # a completely separate object, own data
print(your_dog.name) # "Bella"
print(my_dog.name) # "Rex" — unaffected by your_dogWhat Happens When You Call Dog("Rex", "Labrador", 3)
─────────────────────────────────────────
1. Python creates a new, empty Dog object in memory.
2. Python calls __init__(self, "Rex", "Labrador", 3)
— `self` is automatically bound to that new object.
3. Inside __init__, self.name = "Rex" attaches the data TO that object.
4. The fully initialized object is returned and assigned to my_dog.
─────────────────────────────────────────
__init__ is called a constructor, though technically the object is already created by the time __init__ runs — __init__ initializes it. This distinction matters more once you meet __new__ in advanced material, but "constructor" is the term you'll hear used in practice.
4. Understanding self
self refers to the specific instance the method is being called on — it's how an object accesses its own data and methods.
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says Woof!") # self.name refers to THIS dog's name
rex = Dog("Rex")
bella = Dog("Bella")
rex.bark() # "Rex says Woof!"
bella.bark() # "Bella says Woof!" — same method, different selfBehind the Scenes
─────────────────────────────────────────
rex.bark()
is EXACTLY equivalent to:
Dog.bark(rex)
↑
`self` is just the first parameter, and Python passes the
instance you called the method ON as that first argument automatically.
─────────────────────────────────────────
Common beginner mistake: forgetting self as the first parameter of a method:
class Dog:
def bark(): # ✗ missing self!
print("Woof!")
rex = Dog()
rex.bark() # TypeError: bark() takes 0 positional arguments but 1 was given
# Python still tries to pass `rex` as the first argument!5. Instance Attributes vs Class Attributes
Instance attributes belong to one specific object (set via self.x = ..., usually in __init__). Class attributes are shared by all instances of the class.
class Dog:
species = "Canis familiaris" # CLASS attribute — same for every Dog
def __init__(self, name, age):
self.name = name # INSTANCE attribute — unique per Dog
self.age = age # INSTANCE attribute — unique per Dog
rex = Dog("Rex", 3)
bella = Dog("Bella", 5)
print(rex.species) # "Canis familiaris"
print(bella.species) # "Canis familiaris" — same value, shared from the class
print(rex.name) # "Rex"
print(bella.name) # "Bella" — different, each instance has its ownMemory Model
─────────────────────────────────────────
Dog (class)
└── species = "Canis familiaris" ← ONE copy, shared
rex (instance) bella (instance)
├── name = "Rex" ├── name = "Bella" ← each instance has its OWN copy
└── age = 3 └── age = 5
─────────────────────────────────────────
Trap: modifying a class attribute through an instance actually creates a new instance attribute, shadowing the class attribute — it does NOT change the shared value:
rex.species = "Modified" # this creates an INSTANCE attribute on rex only
print(rex.species) # "Modified"
print(bella.species) # "Canis familiaris" — unaffected!
print(Dog.species) # "Canis familiaris" — the class attribute is untouched
# To actually change the shared value for ALL instances:
Dog.species = "Changed for everyone"
print(bella.species) # "Changed for everyone" — now it changedRule of thumb: use class attributes for genuinely shared, constant-like data (like species here); use instance attributes for anything that varies per object.
6. Instance Methods
Regular methods (like bark() above) automatically receive self and can read/modify that specific instance's data:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f"Deposited {amount}. New balance: {self.balance}")
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds")
return
self.balance -= amount
print(f"Withdrew {amount}. New balance: {self.balance}")
def __str__(self):
"""Human-readable representation — covered fully in Chapter 7."""
return f"BankAccount(owner={self.owner}, balance={self.balance})"
account = BankAccount("Alice", 100)
account.deposit(50) # Deposited 50. New balance: 150
account.withdraw(30) # Withdrew 30. New balance: 120
print(account) # BankAccount(owner=Alice, balance=120)7. Class Methods & Static Methods
Two special kinds of methods that don't operate on a specific instance in the usual way:
class Dog:
species = "Canis familiaris"
total_dogs = 0
def __init__(self, name, age):
self.name = name
self.age = age
Dog.total_dogs += 1
@classmethod
def from_birth_year(cls, name, birth_year):
"""Alternative constructor — receives the CLASS (cls), not an instance."""
current_year = 2026
age = current_year - birth_year
return cls(name, age) # cls(...) is the same as Dog(...)
@staticmethod
def is_valid_age(age):
"""Doesn't need self OR cls — just a utility function related to the class."""
return 0 <= age <= 30
rex = Dog.from_birth_year("Rex", 2023) # alternative way to build a Dog
print(rex.age) # 3 (as of 2026)
print(Dog.is_valid_age(5)) # True
print(Dog.is_valid_age(-1)) # False
print(Dog.total_dogs) # 1| Decorator | First Parameter | Use Case |
|---|---|---|
| (none) — instance method | self (the instance) | Default — operates on one object's data |
@classmethod | cls (the class itself) | Alternative constructors, or logic touching class-level state |
@staticmethod | (neither) | A utility function that's logically related but needs no instance/class data |
8. Summary & Next Steps
Key Takeaways
- A class is a blueprint; an object/instance is a concrete thing built from it — every value you've used since Module 1 (strings, lists, dicts) is already an object of some built-in class.
__init__runs automatically on creation, setting up instance data viaself.attribute = value.selfis simply the instance the method was called on — Python passes it automatically as the first argument.- Class attributes are shared across all instances; instance attributes are unique per object — assigning
instance.attr = xcreates a new instance attribute rather than modifying the shared class one. @classmethod(receivescls) is typically used for alternative constructors;@staticmethodis a utility function grouped with the class for organization, needing neitherselfnorcls.
Concept Check
- What's the difference between a class and an instance?
- Why does
rex.bark()work even thoughbark(self)is defined with a parameter, but you only wroterex.bark()with no arguments? - If
Dog.species = "X"and thenrex.species = "Y", what doesbella.speciesprint, and why?
Next Chapter
→ Chapter 2: Inheritance & super()
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index