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.
- Write a class with
__init__and instance attributes. - Explain what
selfis 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.
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.
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.
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
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.self.something. There is no separate declaration.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 directlyEvery 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
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.
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.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
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.
# 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.
• 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.
Why does s.add_grade(80) pass two arguments to a method
defined with two parameters?
Answer
s is passed automatically as self. The call is
really Student.add_grade(s, 80).What does print(Point(1, 2)) show without
__str__?
Answer
<__main__.Point object at 0x7f...> — the type and a
memory address.Write a Rectangle class with width, height, an
area method and a readable __str__.
Solution
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)What goes wrong here?
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__.Why might Point(1, 2) == Point(1, 2) be False?
Answer
__eq__
is not defined, == compares identity, and these are two
distinct objects.Rewrite distance(p, q) as a method on
Point.
Solution
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.5p.distance_to(q), which reads as the operation
belonging to a point.Which suits a phone book mapping names to numbers?
Answer
Track how many Student objects have been created.
Solution
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 + 1Student, 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.
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.
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.
__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.
What does __init__ return?
Guide
None. It
initialises the object that is already being created; it does not produce
it.Is self a keyword?
Guide
Where should mutable attributes be created?
Guide
__init__, so each instance gets its own. In the class body
they would be shared by all instances.When is a dictionary better than a class?