CSC108H1/Unit 7
Unit 7 · Intro to Programming

Classes, Objects & Methods

So far you have used types Python provides. A class lets you define your own: bundle the data that belongs together, and the operations that make sense on it, into one named thing. Everything you have already used — strings, lists, dictionaries — is a class.

By the end you can
  • Write a class with __init__ and instance attributes.
  • Explain what self is and why every method takes it.
  • Implement __str__ and __eq__ and say when each is called.
  • Decide when a class earns its place over a dictionary.
Why it matters & where it comes from
Why it exists

Passing four related values into every function is fragile: swap two arguments and nothing complains. A class names the bundle once, so the grouping is checked and the operations live beside the data they act on.

In the real world

Every library you import is built from classes. A datetime, a database connection, a plot — all objects with methods. Reading their documentation is reading class definitions.

Story & open questions

Objects came from Simula in the 1960s, built to simulate real-world entities. Python's version is deliberately light: no privacy enforcement, no required declarations. The leading underscore convention for "internal" is an agreement between programmers, not a rule the language enforces.

Defining a class

Example A complete small class
python
class Student:
    """A student with a name and a list of grades."""

    def __init__(self, name: str) -> None:
        """Initialize a student named name with no grades yet."""
        self.name = name
        self.grades = []

    def add_grade(self, mark: float) -> None:
        """Record mark for this student."""
        self.grades.append(mark)

    def average(self) -> float:
        """Return this student's mean grade, or 0.0 if there are none."""
        if not self.grades:
            return 0.0
        return sum(self.grades) / len(self.grades)
class introduces the type; Student is capitalised by convention.
__init__ runs when an instance is created. It does not return the object — it sets up the one already being made.
Attributes are created simply by assigning to self.something. There is no separate declaration.
Example Using it
python
s = Student('Ada')     # calls __init__ with self = the new object
s.add_grade(80)
s.add_grade(90)
print(s.average())     # 85.0
print(s.name)          # 'Ada' - attributes are readable directly
Rule self is the instance

Every method's first parameter is the object it was called on. Writing s.add_grade(80) is Python calling Student.add_grade(s, 80). So self is not magic and not a keyword — it is an ordinary parameter that everyone agrees to call self.

Forgetting it gives TypeError: add_grade() takes 1 positional argument but 2 were given, which is confusing until you know the object is being passed invisibly.

Special methods

Definition

Methods named with double underscores hook into Python's built-in behaviour. Defining them lets your class work with the ordinary syntax rather than requiring special function calls.

Example The three worth writing
python
class Point:
    """A point in two dimensions."""

    def __init__(self, x: float, y: float) -> None:
        """Initialize a point at (x, y)."""
        self.x = x
        self.y = y

    def __str__(self) -> str:
        """Return a readable description of this point.

        >>> str(Point(1, 2))
        '(1, 2)'
        """
        return '(' + str(self.x) + ', ' + str(self.y) + ')'

    def __eq__(self, other) -> bool:
        """Return whether this point has the same coordinates as other.

        >>> Point(1, 2) == Point(1, 2)
        True
        """
        return self.x == other.x and self.y == other.y
__str__ is what print and str() use. Without it you get <__main__.Point object at 0x10a...>, which tells the reader nothing.
__eq__ is what == uses. Without it, == falls back to identity, so two points with identical coordinates compare unequal — the same is-versus-== distinction from Unit 5.
Without __eq__, equality is identity
python
Point(1, 2) == Point(1, 2)   # False, if __eq__ is not defined

Two separately built objects are different objects. This bites hardest when testing, because an assertion that looks obviously true fails.

When a class is the right choice

Rule Class or dictionary?
Use a dictionary when the keys are data — unknown until runtime, varying between instances, arbitrary in number.
Use a class when the fields are fixed and known while writing the code, and when operations belong with the data.

A student record with exactly a name and grades is a class. A word-count table is a dictionary. Writing d['nmae'] silently creates a new key; writing s.nmae raises AttributeError immediately, and that early failure is much of the value.

Example Operations live with the data
python
# without a class: the function and the data drift apart
def student_average(grades: list) -> float:
    ...
avg = student_average(record['grades'])

# with a class: there is one place to look
avg = student.average()

The second version cannot be called on the wrong thing, and someone reading Student sees every operation that applies to it.

Common pitfalls

• Forgetting self in a method signature, or forgetting self. when referring to an attribute inside a method.
• Returning something from __init__. It initialises; it must return None.
• Assigning a mutable default in the class body rather than in __init__ — it is then shared by every instance, exactly the Unit 3 default-argument trap.
• Defining __str__ and expecting it inside a list. Printing a list of objects uses __repr__, not __str__.

Practice

Predict each answer before running it. Original exercises written for this guide in the style of the course's weekly sets.

Practice 1 · What is self?

Why does s.add_grade(80) pass two arguments to a method defined with two parameters?

Answer
Because s is passed automatically as self. The call is really Student.add_grade(s, 80).
Practice 2 · Missing __str__

What does print(Point(1, 2)) show without __str__?

Answer
Something like <__main__.Point object at 0x7f...> — the type and a memory address.
Practice 3 · Write a class

Write a Rectangle class with width, height, an area method and a readable __str__.

Solution
python
class Rectangle:
    """A rectangle with a width and a height."""

    def __init__(self, width: float, height: float) -> None:
        """Initialize a rectangle of the given width and height."""
        self.width = width
        self.height = height

    def area(self) -> float:
        """Return the area of this rectangle.

        >>> Rectangle(3, 4).area()
        12
        """
        return self.width * self.height

    def __str__(self) -> str:
        """Return a readable description of this rectangle."""
        return str(self.width) + 'x' + str(self.height)
Practice 4 · Shared mutable

What goes wrong here?

python
class Basket:
    items = []
    def add(self, x) -> None:
        self.items.append(x)
Answer
items is defined on the class, so every basket shares one list. Adding to one basket adds to all. Create it per instance inside __init__.
Practice 5 · Equality

Why might Point(1, 2) == Point(1, 2) be False?

Answer
If __eq__ is not defined, == compares identity, and these are two distinct objects.
Practice 6 · Method versus function

Rewrite distance(p, q) as a method on Point.

Solution
python
    def distance_to(self, other) -> float:
        """Return the distance from this point to other.

        >>> Point(0, 0).distance_to(Point(3, 4))
        5.0
        """
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
Called as p.distance_to(q), which reads as the operation belonging to a point.
Practice 7 · Class or dict?

Which suits a phone book mapping names to numbers?

Answer
A dictionary. The keys are data — names are not known while writing the code, and there can be any number of them.
Practice 8 · Counting instances

Track how many Student objects have been created.

Solution
python
class Student:
    count = 0          # class attribute, shared deliberately

    def __init__(self, name: str) -> None:
        """Initialize a student named name."""
        self.name = name
        Student.count = Student.count + 1
This is the legitimate use of a class attribute: data belonging to the type itself rather than to any one instance. Note it is updated through Student, not self.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC108H1 topic map. It should cover the following items before you move on:

  • Class definition. class, __init__, instance attributes.
  • Methods. self, calling convention, and docstrings on every one.
  • Special methods. __str__ and __eq__, and what calls each.
  • Design judgment. When a class beats a dictionary.
Textbook/source alignment

The calendar lists "classes, objects, methods" under program structure. Assignments in this course typically require a class with full docstrings; check the handout for the exact style required.

Core idea map

A class is a type you define. Instances hold their own attributes; methods are functions that receive the instance as self. Unit 8 tests these the same way it tests plain functions.

Study routine. Write __str__ on every class you define, immediately. Debugging without it means reading memory addresses.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · __init__

What does __init__ return?

Guide
None. It initialises the object that is already being created; it does not produce it.
Extension 2 · self

Is self a keyword?

Guide
No, just the conventional name for the first parameter. Python passes the instance there automatically.
Extension 3 · Attribute location

Where should mutable attributes be created?

Guide
Inside __init__, so each instance gets its own. In the class body they would be shared by all instances.
Extension 4 · Choosing

When is a dictionary better than a class?

Guide
When the keys are data rather than a fixed schema — unknown at authoring time and varying in number.