Grade by Grade
C. Designing Modular Programs · Modular and Object-Oriented Design

Classes as Building Blocks: Constructors, Methods and Class Attributes

C1.2 A2.1 A2.2About 75 minutes
Learning goals
  • I can explain the difference between a class and an object, and between an attribute and a method.
  • I can write a class with a constructor (__init__), instance attributes, methods and a __str__ method.
  • I can create several objects from one class and predict how calling a method changes an object's state.
  • I can tell instance attributes from class attributes, and write a class method as an alternative constructor.
  • I can put a class in its own module and import it into another file.

Words to know

class
A programmer-defined type: a blueprint that says what data (attributes) and actions (methods) its objects have.
object (instance)
One value built from a class, with its own copy of the instance attributes.
instantiation
Creating an object by calling the class like a function, e.g. TransitCard("Ana", 500).
constructor
In Python, the __init__ method, which runs automatically when an object is created and sets its starting attributes.
self
The first parameter of every method: the particular object the method was called on.
attribute
A variable that belongs to an object (instance attribute) or to the class itself (class attribute).
method
A function defined inside a class, called on an object with dot notation, e.g. ana.tap().
state
The current values of an object's attributes.
class method
A method marked with @classmethod that receives the class (cls) instead of an object; often used as an alternative constructor.
command / query
A command method changes an object's state; a query method returns information and changes nothing.

From data holders to building blocks

In the first unit you grouped data into records and wrote your first classes, such as a school-store item that refuses to sell more than it has. In this unit the class becomes the main unit of design: the piece you plan with CRC cards, protect with encapsulation, put in its own module, extend with inheritance and reuse in other programs.

To design good classes you need full control of their parts: constructors with sensible defaults, methods that are clearly commands or queries, class attributes shared by every object, and class methods that offer other ways to build an object. This lesson works through those parts with one example, a city's transit fare card. Bundling data with the code that uses it is data encapsulation, expectation C1.2 of this unit.

Remember from Data Types, Files and Expressions

In One-Dimensional Arrays of Records you defined records with dataclasses, and in Arrays of Objects: State, Behaviour and References you wrote a class with __init__, self and methods, kept a list of objects, and saw that variables hold references (so b = a makes an alias, and is tests identity). In Grade 11 you drew UML class boxes. This lesson builds on all of that rather than repeating it.

The parts of a class

  • class TransitCard: starts the definition. Class names use CapWords (each word capitalized), unlike function names.
  • __init__(self, ...) is the constructor. Python calls it automatically when you write TransitCard("Ana", 500), passing the new object as self and your arguments after it.
  • Instance attributes are created by assigning to self.something inside a method, usually in __init__. Every object gets its own set.
  • Methods are functions defined inside the class. Their first parameter is always self. You call them on an object: ana.tap(). Python turns that into TransitCard.tap(ana), which is how self gets its value.
  • A class attribute, such as FARE = 330, is written directly in the class body. There is only one copy, shared by all objects.
  • __str__(self) returns the text that print(obj) and str(obj) show. Without it, print shows something unhelpful like <__main__.TransitCard object at 0x7f...>.
TransitCardFARE = 330ownerbalancerides__init__(owner, balance)load(cents)tap()__str__()the class (blueprint)ana : TransitCardowner = "Ana"balance = 170rides = 1ben : TransitCardowner = "Ben"balance = 670rides = 1objects (instances)instantiateowncopy ofthe dataowncopy ofthe data
One class, many objects. The class (left, in UML style) defines the attributes and methods once. Each object made from it has its own owner, balance and rides; FARE belongs to the class and is shared. These values are the state after the program in Example 1 runs (balances in cents).
Worked example

Example 1: A transit card class

Write a TransitCard class. A card has an owner, a balance and a ride count. You can load money onto it, and tapping it pays one fare (330 cents in this example city) only if the balance is high enough. Then create two cards and use them.

  1. Store money in whole cents. Floats cannot store most decimal amounts exactly (you saw this in the Data Types unit), so 330 cents is safer than 3.30 dollars. Only __str__ converts to dollars for display.
  2. Constructor: __init__(self, owner, balance=0) stores the owner and starting balance, and sets rides to 0. The default parameter lets you create a card with no money: TransitCard("Ben").
  3. A command method: load(self, cents) changes the state and returns nothing.
  4. A query-and-command method: tap(self) checks the balance against the shared TransitCard.FARE. If there is enough, it subtracts the fare, counts the ride and returns True; otherwise it changes nothing and returns False.
  5. Trace: Ana starts with 500. First tap: 500 ≥ 330, so 170 left, True. Second tap: 170 < 330, so False and nothing changes. Ben starts at 0, loads 1000, taps: 670 left, True.
  6. vars(ben) shows the object's instance attributes as a dictionary, a handy debugging tool. FARE is not in it, because FARE belongs to the class.

The program and its output are below.

class TransitCard:
"""A reloadable transit fare card. Money is stored in whole cents."""
FARE = 330 # class attribute: one value shared by every card
def __init__(self, owner, balance=0):
"""Create a card for owner with a starting balance in cents."""
self.owner = owner # instance attributes:
self.balance = balance # each card object has its own
self.rides = 0
def load(self, cents):
"""Add money (in cents) to the card."""
self.balance += cents
def tap(self):
"""Pay one fare if possible. Return True if the rider may board."""
if self.balance >= TransitCard.FARE:
self.balance -= TransitCard.FARE
self.rides += 1
return True
return False
@classmethod
def from_text(cls, line):
"""Alternative constructor: build a card from "owner,cents" text."""
owner, cents = line.split(",")
return cls(owner.strip(), int(cents))
def __str__(self):
"""Return a readable description, used by print() and str()."""
return f"{self.owner}: ${self.balance / 100:.2f}, rides: {self.rides}"
ana = TransitCard("Ana", 500)
ben = TransitCard("Ben")
print(ana.tap(), ana.tap())
ben.load(1000)
print(ben.tap())
print(ana)
print(ben)
print(vars(ben))

transit.py with a short demo at the bottom.

True False
True
Ana: $1.70, rides: 1
Ben: $6.70, rides: 1
{'owner': 'Ben', 'balance': 670, 'rides': 1}

Output of the program above (what Python prints).

Statementana.balanceana.ridesben.balanceben.ridesReturned
ana = TransitCard("Ana", 500)5000
ben = TransitCard("Ben")500000
ana.tap()170100True
ana.tap()170100False
ben.load(1000)170110000None
ben.tap()17016701True
A state trace table for Example 1. Each method call changes only the object it was called on.

Class attributes and class methods

Some data belongs to every object of a class at once: the fare, a tax rate, a maximum size. Store it once as a class attribute and read it as TransitCard.FARE. A change to the class attribute is seen by every object immediately, which is exactly what a fare increase needs. (When Python reads self.FARE, it looks in the object first and then in the class, which leads to a trap explored in the Challenge questions.)

Sometimes objects need to be built from a different kind of input, such as a line of a file. A class method, marked with @classmethod, receives the class itself as its first parameter, named cls by convention, and can call cls(...) to build and return a new object. This is called an alternative constructor; the standard library uses the same idea in dict.fromkeys and datetime.date.fromisoformat. Using cls instead of the class name means a subclass that inherits the method builds objects of the subclass.

class TransitCardFARE = 330 (one copy)anabalance = 170benbalance = 670kaibalance = 1000Each object has its own balance; all of them look up FARE in the class.Change TransitCard.FARE once and every card uses the new fare.
Instance attributes live in each object; a class attribute lives once, in the class. When code reads self.FARE or TransitCard.FARE, Python finds the single shared value.
Worked example

Example 2: A class in its own module, with an alternative constructor

Save the TransitCard class (without the demo lines) as transit.py. In a second file, build cards from text lines such as "Kai, 1000" with the class method from_text, tap each card once, then raise the fare to 350 cents and tap the first card again.

  1. from transit import TransitCard imports the class by name. A class is reused across files exactly like a function (A2.1).
  2. TransitCard.from_text(line) splits the line at the comma, strips the spaces from the name, converts the cents with int(), and returns cls(owner, cents), a new card.
  3. Kai starts with 1000 cents: the tap succeeds and leaves 670. Noor has 250 cents, less than 330, so the tap fails.
  4. TransitCard.FARE = 350 changes the one shared value. Kai's next tap charges 350: 670 − 350 = 320 cents, shown as $3.20, with 2 rides.

Output: Kai True, Noor False, then True Kai: $3.20, rides: 2.

from transit import TransitCard
cards = [TransitCard.from_text(line) for line in ["Kai, 1000", "Noor,250"]]
for c in cards:
print(c.owner, c.tap())
TransitCard.FARE = 350 # a fare increase: one change, every card sees it
print(cards[0].tap(), cards[0])

main.py, saved in the same folder as transit.py.

Kai True
Noor False
True Kai: $3.20, rides: 2

Output of the program above (what Python prints).

Commands and queries

A useful design habit: make each method either a query (returns information and changes nothing, like a get_balance() method) or a command (changes the state, like load()). tap() does both because the caller needs to know whether boarding is allowed; when a method mixes the two, its docstring must say so clearly.

Just for fun

A class walks into a bar and orders a drink. The bartender asks, "For yourself?"

The class says, "Always. Every one of my methods starts with self."

Hands-on activity

Model a household object

You need: an object from home with changing state (a bookshelf, a water bottle with mL markings, a thermostat, a board game), paper, and a computer with Python.

  1. Choose the object. On paper, list its attributes (for a water bottle: capacity_ml, current_ml) and its actions (fill, drink(ml), is_empty).
  2. Draw a UML class box with three compartments.
  3. Write the class in a file of its own. The constructor sets the starting state. Each action is a method; make sure a method cannot create an impossible state (for example, drinking more than is in the bottle should only empty it, not make it negative).
  4. In a second file, create two objects, call methods on each, and print them after each call. Add a __str__ so print gives readable output.
  5. Make a state trace table like the one in this lesson for your calls, then run the program and compare.

What to look for: CapWords class name, every method has self first, each object keeps its own state, and the trace table matches the output.

Your turn

1

In one sentence each: what is a class, and what is an object?

2

What is wrong with this method?

class Dog:

    def __init__(self, name):

        self.name = name

    def speak():

        return self.name + " says woof"

3

Using TransitCard, predict:

c = TransitCard("Lee", 700)

c.tap()

c.tap()

c.tap()

print(c.balance, c.rides)

4

Write a method refund(self) for TransitCard that adds one fare back to the balance and subtracts one ride, but only if rides is greater than 0.

5

What is the difference between FARE and self.balance in TransitCard?

6

Using from_text, what card does TransitCard.from_text("Mo ,400") build? What is its balance after one tap?

7

Why does the class store money in cents rather than dollars?

More practice: warm-up

1

What is the name of the constructor method in Python?

2

What is the first parameter of every ordinary method?

3

What naming style do class names use?

4

Which method controls what print(obj) shows?

5

True or false: two objects of the same class always share the same instance attribute values.

More practice: on your own

1

Write a class Rectangle with attributes width and height, a method area() and a method perimeter(). What do they return for a 4 by 2.5 rectangle?

2

Predict:

class Counter:

    def __init__(self):

        self.n = 0

    def click(self):

        self.n += 1

        return self.n

a = Counter()

b = Counter()

a.click()

a.click()

print(b.click(), a.click())

3

Add a __str__ to Rectangle so that print(Rectangle(4, 2.5)) shows Rectangle 4 x 2.5.

4

A program has owners, balances and rides as parallel lists. Give two advantages of replacing them with one list of TransitCard objects.

5

Explain what Python does, step by step, for ben = TransitCard("Ben").

Challenge

1

Predict, and explain:

class Card:

    FARE = 330

c1 = Card()

c2 = Card()

c1.FARE = 0

Card.FARE = 350

print(c1.FARE, c2.FARE)

2

Write a class Playlist that stores a name and a list of song titles. The constructor must give each playlist its own empty list. Explain why def __init__(self, name, songs=[]): would be a bug.

3

A Thermostat class has self.target that must stay between 10 and 30 °C. Write set_target(self, t) that enforces this, and say which design idea from this lesson it shows.

Check your understanding

Quiz

1.

What does __init__ do?

2.

In ana.tap(), what is the value of self inside tap?

3.

Which is a class attribute in TransitCard?

4.

Which decorator makes a method that receives the class as cls, useful as an alternative constructor?

5.

p = Rectangle(3, 5) where area() returns width times height. What is p.area()?

6.

A class has no __str__ method. What does print(obj) show?

For parents and teachers

Big idea: Expectation C1.2 asks students to apply data encapsulation (classes, records, structures) in program design. A class bundles data with the methods that use it. Students wrote first classes in the Data Types, Files and Expressions unit; this lesson focuses on the parts of a class used in design (constructors, commands and queries, class attributes, class methods). The next lesson covers protecting the data. Writing the class in its own file also practises A2.1 (programs across multiple files), and the class itself is the first tool of A2.2 (reusable design).

How to help: Ask your teen to explain, with a real object from home, which parts are attributes and which are methods. The state trace table is the single most useful habit: have them predict each line before running the program.

Common difficulties:

  • Forgetting self as the first parameter, or forgetting self. in front of attribute names (which makes a local variable that disappears).
  • Confusing the class (the blueprint) with an object (one instance).
  • Writing self.FARE = ... inside a method, which creates a per-object value instead of changing the shared class attribute.
  • Using a mutable default such as songs=[] in a constructor.