MA-PY1 · Module 3.1
Objects and classes
Topics: OOP · Classes · Methods
Overview
Module 3.1 — Objects and classes
Scroll Core ideas → worked snippets → Practice (quiz). Attempt each question before Discuss / reveal. Use any Python REPL, script, or notebook you like.
Concepts
Core ideas — bundling state and behaviour with classes
So far you have written functions that take data as arguments and return results. That works well for small scripts. As programs grow, you often notice the same problem: related data and the operations on that data get passed around separately, duplicated in every function signature, or stored in globals that are hard to reason about.
Classes solve that by letting you define a new type — a blueprint that bundles attributes (the data a thing carries) and methods (functions written to work on that data). A class is the recipe; an object (also called an instance) is one concrete thing built from that recipe.
From plain data to a class
Imagine tracking a simple counter. Without classes you might write:
def make_counter(start):
return {"value": start}
def incr(counter, step=1):
counter["value"] += step
return counter["value"]
The counter “lives” in a dictionary, and you pass that dictionary into every function. It works, but the connection between counter and incr is easy to break — nothing stops you from calling incr(some_unrelated_dict, 5).
A class keeps the data and the operations in one place:
class Bump:
def __init__(self, start=0):
self.value = start
def incr(self, step=1):
self.value += step
return self.value
class Bump:defines a new type namedBump.__init__runs when you construct a new object. It sets up the object’s starting state.selfinside the class body refers to the particular object being built or updated.self.value = startcreates an instance attribute — data that belongs to this object only.
You create an object by calling the class like a function:
a = Bump(10) # a is an object (instance) of type Bump
a.incr() # call a method on that object
print(a.value)
Here a is an object. Bump is the class. Saying “a is an instance of Bump” means a was created from the Bump blueprint and has its own value.
Attributes, methods, and dot notation
Once you have an object, you access its data and behaviour with dot notation:
| Syntax | Meaning |
|--------|---------|
| obj.attr | read an attribute (data stored on the object) |
| obj.method(args) | call a method (a function defined on the class, operating on this object) |
A method looks like a function defined inside the class, but its first parameter is always the receiver — the object the call is “on”. By convention that parameter is named self.
Objects and self
Here is a slightly richer example — a tiny wallet class we will reuse throughout this module:
class StudyWallet:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
Why StudyWallet and not just Wallet? The Study prefix is a habit in this course for practice types — small invented classes we use to teach an idea, not a library you import. You will see the same pattern soon with StudyLog. In your own projects, a plain name like Wallet or Account is perfectly normal; Python does not require the prefix.
First we define the class (the blueprint). Then we create an object and call a method:
acct = StudyWallet(100) # construct one wallet; acct is an object
acct.deposit(50) # ask that object to run its deposit method
print(acct.balance) # 150
Nothing magical happened before this point — acct did not exist until StudyWallet(100) ran. deposit is just a method that adds to self.balance.
When you write acct.deposit(50), Python automatically passes acct as the first argument to deposit. These two lines are equivalent:
acct.deposit(50)
StudyWallet.deposit(acct, 50)
That is what people mean when they say acct.deposit(50) is syntactic sugar for StudyWallet.deposit(acct, 50): you write the short form; Python supplies the receiver instance as self.
selfis only a naming convention for that first parameter. You might occasionally seemeorthisin other codebases — stick withselfin your own classes unless you have a strong reason not to.- Instance attributes (such as
self.balance) belong to one object. Two wallets do not share one balance unless you deliberately design that (see class attributes below).
__init__ is initialisation—not “allocation magic”
When you call StudyWallet(100), Python allocates an object, then calls __init__(self, 100), which sets self.balance = 100. Use __init__ to validate arguments, initialise containers, attach timestamps, and raise on invalid combinations.
No separate “declaration” keyword for fields—whatever you self.whatever = first time becomes the attribute footprint.
Method vs plain function vs static pattern
Inside the class keyword def with self → bound method.
You will later encounter @staticmethod and @classmethod — ignore until orchestration complexity demands.
Representation — __str__ vs __repr__
repr(obj)targets developers: unambiguous, often echoingeval-able-ish constructor text.str(obj)targets humans in UI/logs.
Define __repr__ first; add __str__ only when prettier wording buys clarity.
Encapsulation in Python culture
Traditionally _underscored names signal “please treat as internal.” Name mangling on __dunder exists but is not armour—tests and discipline matter more than hiding.
Prefer thin public APIs: few methods manipulate few fields cleanly.
Class attributes vs instance attributes
self.x = …attaches data to this instance only.- A name assigned in the class body but not through
self(e.g.class Widget: counter = 0) is a class attribute—one slot shared by all instances unless an instance later shadows it withself.counter.
Danger: class Bad: items = [] shares one mutable list across instances. Give each object its own list in __init__: self.items = [].
Designing for self-study
Start every class sketch with sentences:
- What noun does it model?
- What fields constitute minimal consistent state?
- Which methods mutate state (commands) versus report derived facts (queries)?
Some designers call query/command separation—not law for toy code, helpful mental scaffolding.
Self-study checklist
- Instantiate two objects; mutate one—prove other unaffected.
- Articulate why
def __init__(self, items=[]):is unsafe; useitems=Noneand build a newlistper call. - Decide what
reprshould expose forPoint(3, 4).
Examples
Worked snippets — objects, methods, lifecycle
Minimal class with __init__ and a method
class Bump:
"""A tiny counter you can step forward."""
def __init__(self, start=0):
if start < 0:
raise ValueError("start must be non-negative")
self.value = start
def incr(self, step=1):
self.value += step
return self.value
a = Bump(10)
print(a.incr(), a.incr(5))
Instances are independent
u = Bump(2)
v = Bump(2)
u.incr(100)
print(u.value, v.value) # only u moved
__repr__ for debugging-friendly output
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
p = Point(3, 4)
print([p]) # list shows repr, not memory address
Optional user string:
def __str__(self):
return f"({self.x}, {self.y})"
Query vs command split (readability pattern)
class Rect:
def __init__(self, w, h):
if w <= 0 or h <= 0:
raise ValueError("dimensions must be positive")
self.w = w
self.h = h
def area(self):
return self.w * self.h
def scale(self, factor):
"""Mutate both sides by the same multiplicative factor."""
if factor <= 0:
raise ValueError("factor")
self.w *= factor
self.h *= factor
r = Rect(2, 3)
print(r.area())
r.scale(2)
print(r.w, r.h, r.area())
Guarding invalid amounts (expanded)
class StudyWallet:
def __init__(self, balance=0):
if balance < 0:
raise ValueError("balance")
self.balance = balance
def deposit(self, amount):
if amount < 0:
return False
self.balance += amount
return True
def withdraw(self, amount):
if amount < 0 or amount > self.balance:
return False
self.balance -= amount
return True
Class attribute vs instance list (contrast)
class Shared:
stash = [] # everyone shares this list object
class Separate:
def __init__(self):
self.stash = []
s1, s2 = Shared(), Shared()
s1.stash.append(1)
print(s2.stash) # [1] — surprise if unintended
a, b = Separate(), Separate()
a.stash.append(1)
print(b.stash) # [] — independent lists
Practice check — objects and classes
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.Given `acct = StudyWallet(100)`, calling `acct.deposit(50)` is shorthand for which call?
2.What is the main role of `__init__`?
3.Why is this class dangerous? ```python class StudyBasket: items = [] def add(self, x): self.items.append(x) ```
4.After `p = Point(1, 2)` and `q = Point(1, 2)` (two separate instances), changing `p.x = 9` does what to `q.x`?
5.Which statement best describes `__repr__` versus `__str__`?