Python

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

JrCodex·8 min read

Jr Codex Python Notes

Level: Intermediate Prerequisites: Module 3: Modules, Data & Errors Time to complete: ~30 minutes


Table of Contents

  1. What is a Class? What is an Object?
  2. Defining a Class
  3. __init__ — The Constructor
  4. Understanding self
  5. Instance Attributes vs Class Attributes
  6. Instance Methods
  7. Class Methods & Static Methods
  8. 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 class

This 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/variable

3. __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_dog
What 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 self
Behind 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 own
Memory 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 changed

Rule 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
DecoratorFirst ParameterUse Case
(none) — instance methodself (the instance)Default — operates on one object's data
@classmethodcls (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 via self.attribute = value.
  • self is 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 = x creates a new instance attribute rather than modifying the shared class one.
  • @classmethod (receives cls) is typically used for alternative constructors; @staticmethod is a utility function grouped with the class for organization, needing neither self nor cls.

Concept Check

  1. What's the difference between a class and an instance?
  2. Why does rex.bark() work even though bark(self) is defined with a parameter, but you only wrote rex.bark() with no arguments?
  3. If Dog.species = "X" and then rex.species = "Y", what does bella.species print, and why?

Next Chapter

Chapter 2: Inheritance & super()


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index