Classes, Objects, and Methods

EE1021 Programming for Engineers · Lecture 7 · 2026–2027

Basri Erdogan

Lecture Goals

  1. Distinguish a class, object, instance, attribute, and method
  2. Explain why a class keeps related state and behavior together
  3. Initialize independent instances with __init__
  4. Trace which object becomes self during a method call
  5. Write methods that change state, report state, or validate input
  6. Implement __str__ and recognize other special methods
  7. Translate a small class contract into tested Python code

Every Python Value Is an Object

name = "sensor-1"
values = [20.0, 22.0]

name.upper()          # method of a str object
values.append(24.0)   # method of a list object
type(values)          # <class 'list'>
  • every object has a type (its class), an internal data representation, and a set of methods for interacting with it
  • 1234 is an instance of int; "hello" is an instance of str
  • today you define your own types

A List Hides Its Internal Representation

L list object row of memory addresses the values, stored elsewhere in memory length = 3 room = 4 addresses ● 104022083176 empty 1 2 3 at 1040 at 2208 at 3176
  • inside, a list is a row of memory addresses, one for each value
  • the row has empty places, so append() is fast
  • you never see the addresses; you use the interface: L[i], len(L), L.append()
  • a class gives your own data the same deal: the data and its operations stay together, behind an interface

Part 1: Classes and Instances

A Class Defines a New Type

class Coordinate:
    pass
  • Coordinate is the class name and the new type; the indented block defines the type
  • creating the class: define the name and its attributes (once)
  • using the class: create instances and operate on them (many times)
c = Coordinate()
origin = Coordinate()

c and origin are two distinct instances of the same class.

__init__ Establishes Initial State

class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y
  • __init__ — special method Python calls right after it creates the instance; double underscores mark it as special
  • in Python the constructor is the __init__ method; it runs automatically when an object is created, and this course uses the word constructor for it
  • self — parameter that refers to the instance being initialized
  • self.x, self.y — two data attributes stored on every Coordinate object

Creating Instances and Accessing Attributes

c = Coordinate(3, 4)
origin = Coordinate(0, 0)
print(c.x)        # 3
print(origin.x)   # 0
  • Coordinate(3, 4) — creates a new object and passes 3 and 4 to __init__; you never pass self
  • c.x — the dot accesses an attribute of instance c

Data attributes of an instance are called instance variables. Python runs __init__ conceptually as Coordinate.__init__(new_object, 3, 4).

Predict Both Objects’ State

c = Coordinate(3, 4)
origin = Coordinate(0, 0)

c.x = 10

What are c.x, c.y, origin.x, and origin.y now?

c.x is 10, c.y is 4; origin.x and origin.y remain 0. Each __init__ call filled a separate object.

Instance State Must Be Stored on self

Local variables disappear

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

After __init__ returns, the object has no attributes.

Attributes remain with the object

def __init__(self, x, y):
    self.x = x
    self.y = y

self.x survives because it belongs to the object, not to the call.

A list written directly under class, outside __init__, is created once and shared by every object. Write self.readings = [] inside __init__ so each object gets its own list.

Part 2: Methods and self

A Method Is a Function That Belongs to the Class

class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def distance(self, other):
        dx_sq = (self.x - other.x) ** 2
        dy_sq = (self.y - other.y) ** 2
        return (dx_sq + dy_sq) ** 0.5
  • self — the instance the method is called on
  • other — an ordinary parameter; here another Coordinate
  • self.x, other.x — dot notation reads data from either object

Two equivalent calls: c.distance(origin) and Coordinate.distance(c, origin). Python passes the receiver as self automatically.

Which Object Becomes self?

a = Coordinate(0, 0)
b = Coordinate(3, 4)

print(b.distance(a))
print(a.distance(b))

In each call, which object is self and which is other? What is printed?

First call: self is b, other is a. Second call: the roles swap. Both print 5.0, because distance is symmetric.

A Method Can Change Object State

class Battery:
    def __init__(self, capacity_mah):
        self.capacity_mah = capacity_mah
        self.charge_mah = 0

    def charge(self, amount_mah):
        self.charge_mah = min(self.capacity_mah, self.charge_mah + amount_mah)

    def level_percent(self):
        return 100 * self.charge_mah / self.capacity_mah
  • charge is a command: it changes the receiver’s state and returns nothing
  • level_percent is a query: it reports state and changes nothing
  • avoid printing inside reusable methods; return values instead

__str__ Supplies a Readable Interface

>>> pack = Battery(2000)
>>> print(pack)
<__main__.Battery object at 0x7fa918510488>

The default text is uninformative. Define __str__; print calls it for you.

def __str__(self):
    pct = self.level_percent()
    return f"Battery {self.charge_mah}/{self.capacity_mah} mAh ({pct:.0f}%)"
>>> pack.charge(500)
>>> print(pack)
Battery 500/2000 mAh (25%)

__str__ must return a string.

Repair __str__

What is wrong with this implementation, and what does print(pack) do?

def __str__(self):
    print(self.charge_mah)

It prints but returns None, and print(pack) raises TypeError: __str__ returned non-string. Replace the body with a return of an f-string.

Part 3: Contracts, Operators, and Tests

Validate Input in __init__

class Resistor:
    def __init__(self, label, ohms):
        if ohms <= 0:
            raise ValueError("resistance must be positive")
        self.label = label
        self.ohms = ohms
  • a constructor is the right place to reject an object that could never be valid
  • the caller handles it with try / except ValueError, exactly as in Lecture 6
  • after __init__ returns, every Resistor satisfies its contract

A Complete Small Class

class Resistor:
    def __init__(self, label, ohms):
        if ohms <= 0:
            raise ValueError("resistance must be positive")
        self.label = label
        self.ohms = ohms

    def current(self, volts):
        return volts / self.ohms

    def power(self, volts):
        return volts * self.current(volts)

    def __str__(self):
        return f"{self.label}: {self.ohms} ohm"

power reuses self.current: methods call other methods through self.

assert Checks That Something Is True

x = 5
assert x > 0                       # true: nothing happens
assert x > 10, "x is too small"    # false: the program stops
AssertionError: x is too small
  • assert condition, "message" states something you expect to be true
  • true: the program continues silently
  • false: Python stops with AssertionError and prints your message
  • use assert in tests; check user input with if and raise, as in Lecture 6

Test With Two Independent Instances

r1 = Resistor("R1", 220)
r2 = Resistor("R2", 1000)

assert r1.current(5) == 5 / 220
assert r2.current(5) == 0.005
assert str(r1) == "R1: 220 ohm"

try:
    Resistor("R3", 0)
except ValueError:
    print("invalid resistor rejected")

Test normal values, a boundary, an invalid construction, and that one instance does not disturb another.

Special Methods Map Operators to Your Class

  • print, +, -, ==, <, len() each look for a special method
  • define them with double underscores before and after the name
  • implement an operator only when its meaning for your type is natural
  • full list: Python data model reference
special method expression it implements
__str__(self) print(self), str(self)
__add__(self, other) self + other
__sub__(self, other) self - other
__eq__(self, other) self == other
__lt__(self, other) self < other
__len__(self) len(self)

Example: + on a Fraction Type

class Fraction:
    def __init__(self, num, denom):
        self.num = num
        self.denom = denom

    def __str__(self):
        return f"{self.num}/{self.denom}"

    def __add__(self, other):
        top = self.num * other.denom + self.denom * other.num
        bottom = self.denom * other.denom
        return Fraction(top, bottom)
>>> print(Fraction(1, 2) + Fraction(1, 3))
5/6

__add__ returns a new Fraction; neither operand changes.

type() and isinstance() Report the Class

>>> c = Coordinate(3, 4)
>>> type(c)
<class '__main__.Coordinate'>
>>> isinstance(c, Coordinate)
True
>>> type(Coordinate)
<class 'type'>
  • type(c) is the class you wrote
  • isinstance checks membership and is the safe way to test an object’s type
  • the class itself is an object too, of type type

Translate a Contract into a Class

A Motor has a name and a maximum speed in rpm. set_speed(rpm) stores a speed and rejects values above the maximum or below zero. It prints as M1: 1500/3000 rpm.

Identify:

  1. constructor parameters;
  2. instance attributes;
  3. ordinary method and what it validates;
  4. special method;
  5. two test cases, one valid and one invalid.

Parameters: name, max rpm. Attributes: name, max rpm, current speed (starts at 0). set_speed raises ValueError outside 0 … max. __str__ builds the string. Tests: set_speed(1500) then str(m); set_speed(5000) inside try/except.

Exit Ticket

Add to Coordinate and test:

def move(self, dx, dy):
    """Shift this point by dx and dy. Returns nothing."""

Then explain which object becomes self in origin.move(1, 2), and whether move is a command or a query.

Body: self.x += dx and self.y += dy. In origin.move(1, 2), self refers to the object referenced by origin. It is a command: it changes state and returns None.

A Class Is a Tested Contract

  • a class defines a reusable type; each instance holds its own state
  • __init__ establishes valid initial attributes and may reject invalid ones
  • self is the receiver object during a method call
  • methods change state (commands) or return information (queries)
  • __str__, __add__, and other special methods connect your type to Python syntax
  • tests cover normal, boundary, invalid, and independent-instance behavior

In the lab you apply all of this to a new type, a Sensor with readings, that you build and test from a contract.