24.OOP I: Classes & Objects

Learning objectives:

  • Understand what classes and objects are in Python and why they matter
  • Define classes, instantiate objects, and work with attributes and methods
  • Differentiate instance vs. class data and object identity vs. equality
  • Use __init__, __repr__, and __str__ effectively
  • Work with lists of objects, list methods, list comprehensions, and nested lists
  • Follow best practices for clean, pythonic object-oriented code

1) What are classes and objects?

A class defines the blueprint for creating objects (instances). Objects bundle data (attributes) and behavior (methods). Use classes when you want to model entities with state and behavior—for example, a Dog with a name, age, and actions like bark().

2) Defining your first class

Basic syntax:

class ClassName:

    def __init__(self, arg1, arg2):

        self.arg1 = arg1      # instance attribute

        self.arg2 = arg2

 

    def method(self):

        return f”Using {self.arg1} and {self.arg2}”

 

obj = ClassName(‘A’, ‘B’)

print(obj.method())

 

class Dog:

    # class attribute (shared by all instances)

    species = ‘Canis familiaris’

 

    def __init__(self, name, age):

        self.name = name          # instance attribute

        self.age = age

 

    def bark(self):               # instance method

        return f”{self.name} says woof!”

 

fido = Dog(‘Fido’, 5)

print(fido.bark())                # Fido says woof!

print(fido.species)               # Canis familiaris

 

3) Understanding self

Inside instance methods, ‘self’ refers to the current object. Python passes it implicitly when you call methods: obj.method() -> Class.method(obj).

class Counter:

    def __init__(self):

        self.count = 0

    def increment(self):

        self.count += 1   # modifies *this* object’s state

c1 = Counter(); c1.increment(); print(c1.count)  # 1

c2 = Counter(); print(c2.count)                  # 0 (separate instance)

4) Instance vs. class attributes

Instance attributes live on each object (self.*). Class attributes live on the class and are shared by all instances. Modify class attributes via the class, not via self, to avoid shadowing.

class Session:

    active_sessions = 0   # class attribute

 

    def __init__(self, user):

        self.user = user

        Session.active_sessions += 1

 

    def close(self):

        Session.active_sessions -= 1

 

s1 = Session(‘alice’)

s2 = Session(‘bob’)

print(Session.active_sessions)  # 2

s1.close()

print(Session.active_sessions)  # 1

 

5) __init__, __repr__, and __str__

__init__ initializes new instances. __repr__ returns an unambiguous string useful for debugging; __str__ is the user-friendly string. If you only implement __repr__, Python will use it as a fallback for str().

class Point:

    def __init__(self, x: float, y: float):

        self.x = x

        self.y = y

 

    def __repr__(self):

        return f”Point(x={self.x}, y={self.y})”

 

    def __str__(self):

        return f”({self.x}, {self.y})”

 

p = Point(3, 4)

print(repr(p))  # Point(x=3, y=4)

print(str(p))   # (3, 4)

6) Object identity vs. equality

‘is’ checks identity (same object in memory). ‘==’ checks value equality (via __eq__).

class Point:

    def __init__(self, x: float, y: float):

        self.x = x

        self.y = y

    def __repr__(self):

        return f”Point(x={self.x}, y={self.y})”

    def __str__(self):

        return f”({self.x}, {self.y})”

p = Point(3, 4)

print(repr(p))  # Point(x=3, y=4)

print(str(p))   # (3, 4)

7) Encapsulation and properties

Python relies on conventions: public ‘name’, protected ‘_name’, and private ‘__name’ (name-mangled). Use @property to control access/validation.

class Account:

    def __init__(self, owner, balance=0.0):

        self.owner = owner

        self._currency = ‘USD’      # conventionally protected

        self.__pin = ‘1234’         # name-mangled: _Account__pin

        self._balance = float(balance)

    @property

    def balance(self):

        return self._balance

    @balance.setter

    def balance(self, value):

        if value < 0:

            raise ValueError(‘Balance cannot be negative’)

        self._balance = float(value)

acct = Account(‘Ravi’, 100)

print(acct.balance)    # 100.0

acct.balance = 150

print(acct._Account__pin)  # Accessing the mangled name (discouraged)

8) Methods: instance, class, and static (brief)

Instance methods take self and work on one object. Class methods take cls and are often alternate constructors. Static methods are namespaced functions inside the class.

from datetime import datetime

class User:

    def __init__(self, username, created_at=None):

        self.username = username

        self.created_at = created_at or datetime.utcnow()

    @classmethod

    def from_email(cls, email):

        username = email.split(‘@’)[0]

        return cls(username)

    @staticmethod

    def is_valid_username(name):

        return name.isidentifier()

u1 = User(‘alice’)

u2 = User.from_email(‘bob@example.com’)

print(User.is_valid_username(‘bob_1’))  # True

9) Working with lists of objects

Lists are fundamental when handling multiple objects—filtering, grouping, and transforming collections.

dogs = [Dog(‘Fido’, 5), Dog(‘Luna’, 2), Dog(‘Max’, 7)]

 

# Filter seniors (age >= 5)

seniors = [d for d in dogs if d.age >= 5]

print([d.name for d in seniors])  # [‘Fido’, ‘Max’]

 

# Sort by age

by_age = sorted(dogs, key=lambda d: d.age)

print([d.name for d in by_age])

10) Common list methods (cheat sheet)

Here are the list methods you will use most often:

Here are the list methods you will use most often:

Method

Purpose

Quick example

append(x)

Add one item to the end

lst = [1,2]; lst.append(3)  # [1,2,3]

extend(iter)

Add all items from another iterable

lst = [1]; lst.extend([2,3])  # [1,2,3]

insert(i, x)

Insert x at position i

lst = [1,3]; lst.insert(1, 2)  # [1,2,3]

remove(x)

Remove first occurrence of x

lst = [1,2,2]; lst.remove(2)  # [1,2]

pop([i])

Remove and return item at i (default last)

lst = [1,2]; lst.pop()  # returns 2

clear()

Remove all items

lst = [1,2]; lst.clear()  # []

index(x[, start[, end]])

Return index of first x

[10,20,10].index(10)  # 0

count(x)

Count occurrences of x

[1,2,2,3].count(2)  # 2

sort(key=None, reverse=False)

Sort the list in place

lst = [3,1,2]; lst.sort()  # [1,2,3]

reverse()

Reverse the list in place

lst = [1,2,3]; lst.reverse()  # [3,2,1]

copy()

Shallow copy of the list

b = [1,2]; a = b.copy()

11) List comprehensions

List comprehensions provide a concise way to map, filter, and transform iterables. Syntax: [expression for item in iterable if condition].

# Squares of even numbers from 0..9

squares = [n*n for n in range(10) if n % 2 == 0]

print(squares)  # [0, 4, 16, 36, 64]

# Extract names from objects

names = [d.name for d in dogs]

# Compute-and-filter in one pass

adults = [d.name.upper() for d in dogs if d.age >= 3]

# When output can be large or consumed lazily, prefer generator expressions

# to save memory: (expression for item in iterable)

12) Nested lists (2D lists)

Nested lists are lists whose elements are also lists. Useful for matrices, grids, or grouped data.

# Correctly initialize a 3×3 zero matrix

matrix = [[0 for _ in range(3)] for _ in range(3)]

matrix[0][0] = 1

print(matrix)  # [[1,0,0],[0,0,0],[0,0,0]]

 

# Anti-pattern (creates repeated references!)

wrong = [[0]*3]*3

wrong[0][0] = 1

print(wrong)   # [[1,0,0],[1,0,0],[1,0,0]]

 

# Traverse a nested list

for r, row in enumerate(matrix):

    for c, val in enumerate(row):

        pass  # process (r, c, val)

13) Best practices

  • Prefer simple, cohesive classes with clear responsibilities.
  • Use meaningful names and write docstrings for public classes/methods.
  • Implement __repr__ for easier debugging; consider __str__ for friendly output.
  • Avoid mutable default arguments (use None and set inside __init__).
  • Store per-object state on self; use class attributes only for shared state.
  • Use properties to validate or compute attributes without breaking the API.
  • When processing many objects, favor generator expressions to reduce memory.
  • Initialize nested lists with comprehensions, not multiplication, to avoid aliasing.
  • Use type hints to improve readability and tooling (mypy, IDEs).

14) Practice exercises

  1. Create a class Rectangle with width and height, methods area() and perimeter(), and a __repr__.
  2. Write a class BankAccount with deposit() and withdraw() methods and a balance property with validation.
  3. Model a Student with name and grades (list). Add method average(). Use a list comprehension to create 10 students with random grades and filter those with average >= 80.
  4. Build a 5×5 multiplication table using a nested list comprehension.
Scroll to Top
Tutorialsjet.com