Session 23 - Classes & Objects

Session 23 of the Python Fundamentals series, covered in depth. It moves from a dictionary that only holds data to a class that bundles data and behavior together, covering the class keyword, __init__, self, instance attributes, creating instances, and the fact that each instance carries its own state. It also covers the two roles the dot plays - obj.attr to reach a value, and obj.method() to run a method - and what self really is: the instance, handed in automatically. The traps are dropping self from a method signature, which raises a TypeError about positional arguments, reading an attribute that was never set, which raises an AttributeError, and naming a method without parentheses, which gives you a bound method object rather than a call. Every snippet and error message was executed and copied verbatim from CPython 3.12.

Subject: Python Fundamentals · 100 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Classes & Objects

Title

Python Fundamentals - Session 23

Bundle data and behavior into one named thing

2. What you will be able to do

Objectives

You already store related values in dicts. A class goes one step further: it packages data and the actions on that data together. By the end you can:

  1. Write a class with __init__, and store instance attributes on self.
  2. Create instances and see that each keeps its own separate state.
  3. Use the dot two ways: obj.attr to read a value, obj.method() to run a method.
  1. Explain what self is - the instance, passed in automatically.
  2. Read the three classic errors: missing self, missing attribute, and a method used without ().
  3. Refactor a bundle of dict data into a small, tidy class.

3. What survived from Session 22 - Standard Library Tour?

Warm-up

Discussion prompt

Before we open Session 23 - Classes & Objects: without looking back, what was the main idea of Session 22 - Standard Library Tour, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

Session 22 of the Python Fundamentals series: the 'batteries included' tour. A guided, runnable walk through the modules you reach for daily - math (sqrt, floor, ceil, pi), random (randint, choice, shuffle, and seed for reproducibility), datetime (date, timedelta, strftime), pathlib.Path (the / operator, name/suffix/stem, exists), and collections (Counter, defaultdict) - plus a look at os and sys.

4. From Dicts to Classes

Section

Part 1

5. A dict bundles related values

Concept

You have used a dict to keep related facts under one name - an account's owner and balance, say. It holds data, but it does nothing on its own.

dict as a record — A dict groups named values (keys to values). It stores state, but any behavior lives in separate functions elsewhere.

6. Break it if you can: A dict bundles related values

Counterexample

Discussion prompt

You have used a dict to keep related facts under one name - an account's owner and balance, say. It holds data, but it does nothing on its own.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

7. An account as a dict

Worked example

account = {"owner": "Ana", "balance": 100}
print(account["owner"])
print(account["balance"])

The dict groups two facts

Why: owner and balance live together under one name, account.

Read each value by key

Why: Verified by execution: prints Ana then 100.

expressionvalue
account["owner"]Ana
account["balance"]100

8. Fill in: value for An account as a dict

Comparison

Comparison matrix

From An account as a dict: refill the value column from what you know. The rest of the table is as it appeared.

expressionvalue
account["owner"]Ana
account["balance"]100

9. The dict has no behavior of its own

Concept

To deposit money you write a separate function that takes the dict, edits account["balance"], and hopes every caller spells the keys right.

The data and the actions on it are scattered. A class lets you keep them in one place, named together.

10. By analogy: The dict has no behavior of its own

Analogy

Discussion prompt

Explain The dict has no behavior of its own by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

To deposit money you write a separate function that takes the dict, edits account["balance"], and hopes every caller spells the keys right.

11. A class is a blueprint

Intuition

Think of a class as a blueprint for a kind of thing - an Account, a Counter, a Dog. It describes what every one of them has and what every one of them can do.

The blueprint is not a building. From one blueprint you stamp out many actual buildings - the instances - each with its own address and its own state.

12. Teach it back: A class is a blueprint

Explain it

Discussion prompt

Explain A class is a blueprint to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Think of a class as a blueprint for a kind of thing - an Account, a Counter, a Dog. It describes what every one of them has and what every one of them can do.

13. class and __init__

Section

Part 2

14. class makes a new kind of thing

Concept

class Account: defines a brand-new type named Account. Everything indented under it belongs to the class.

class — A blueprint that defines a new type: what data each instance holds (attributes) and what it can do (methods).

15. Take the definitions apart: dict as a record vs class

Definition probe

Sort into buckets

Every line below is part of the definition of dict as a record or of class — one or the other, never both. Put each where it belongs.

dict as a record
A dict groups named values (keys to values).; It stores state, but any behavior lives in separate functions elsewhere.
class
A blueprint that defines a new type; what data each instance holds (attributes) and what it can do (methods).
b1
A dict groups named values (keys to values). It stores state, but any behavior lives in separate functions elsewhere.
b2
A blueprint that defines a new type: what data each instance holds (attributes) and what it can do (methods).

16. Defining a class does not create anything

Concept

Writing class Account: teaches Python the blueprint - it makes no accounts. Just like a def records a function without calling it.

Objects appear only when you call the class, later, with Account(...).

17. __init__ is the setup method

Concept

__init__ is a special method Python runs automatically the moment you create an instance. Its job is to set up that instance's starting attributes.

The double underscores are part of the name - say it 'dunder init'. You never call it directly; creating the object calls it for you.

18. What has to happen first: Define the Account class

Ranking

Put in order

Put the moves of Define the Account class into the order they have to happen.

  1. Lines 1-4 define the blueprint
  2. Line 6 creates one instance
  3. self.owner and self.balance stick to the object

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. No account exists yet - Python just records how to build one.

19. Define the Account class

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(a.owner)
print(a.balance)

Lines 1-4 define the blueprint

Why: No account exists yet - Python just records how to build one.

Line 6 creates one instance

Why: Account("Ana", 100) runs __init__ with owner="Ana", balance=100.

self.owner and self.balance stick to the object

Why: Verified by execution: prints Ana then 100.

expressionvalue
a.ownerAna
a.balance100

20. What each one costs: Define the Account class

Trade off

Comparison matrix

From Define the Account class: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.

expressionvalue
a.ownerAna
a.balance100

21. self names the instance being set up

Concept

Inside __init__, self is the new object being built. self.owner = owner copies the passed-in value onto the object so it survives after setup finishes.

self is just the first parameter - Python fills it in for you. More on exactly how, later.

22. Restore the missing line: The instance knows its own type

Fill the middle

Fill in the blanks

From The instance knows its own type — one line has had its right-hand side removed. Put it back.

class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance

a = Account("Ana", 100)
print(type(a))

Why: self.balance is what everything below it consumes, so the wrong expression here fails later and somewhere else. type(a) reports which class an object came from.

23. The instance knows its own type

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(type(a))

a was stamped from Account

Why: type(a) reports which class an object came from.

Read the output

Why: Verified by execution: the object is an Account.

expressionoutput
type(a)<class '__main__.Account'>

24. Inspect it line by line: The instance knows its own type

Error analysis

Annotate

Walk the callouts on The instance knows its own type. Each one is a place this is easy to get subtly wrong.

  • type(a) reports which class an object came from.
  • Verified by execution: the object is an Account.

25. Instances & Their Own State

Section

Part 3

26. Calling the class makes an instance

Concept

Account("Ana", 100) looks like a function call, and it is one: it builds a fresh object and returns it. Store it in a variable to keep it.

instance (object) — One concrete thing built from a class. Each instance has its own copy of the attributes __init__ set on it.

27. Finish it with less help: Two accounts, two balances

Faded example

Fill in the blanks

Two accounts, two balances, with the scaffolding fading: two lines are gone now — fill both.

class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance

a = Account("Ana", 100)
b = Account("Bo", 5)
print(a.balance)
print(b.balance)

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. a and b are two different accounts stamped from the same blueprint.

28. Two accounts, two balances

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
b = Account("Bo", 5)
print(a.balance)
print(b.balance)

Each call builds a separate object

Why: a and b are two different accounts stamped from the same blueprint.

Their balances do not mix

Why: Verified by execution: prints 100 then 5.

objectownerbalance
aAna100
bBo5

29. Draw the shape of it: Two accounts, two balances

Blank canvas

Draw it

Draw what Two accounts, two balances just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

30. Each instance owns its attributes

Concept

Changing a.balance never touches b.balance. The blueprint is shared; the state is per-object.

This is the whole point of instances: one recipe, many independent things.

31. Where does each piece belong: Session 23 - Classes & Objects

Sorting

Sort into buckets

These are the pieces of Session 23 - Classes & Objects, out of order. Put each one back under the part of the lesson it belongs to.

From Dicts to Classes
A dict bundles related values; An account as a dict; The dict has no behavior of its own
class and __init__
class makes a new kind of thing; Defining a class does not create anything; __init__ is the setup method
Instances & Their Own State
Calling the class makes an instance; Two accounts, two balances; Each instance owns its attributes
s1
From Dicts to Classes is where Session 23 - Classes & Objects puts A dict bundles related values, An account as a dict, The dict has no behavior of its own. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
class and __init__ is where Session 23 - Classes & Objects puts class makes a new kind of thing, Defining a class does not create anything, __init__ is the setup method. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Instances & Their Own State is where Session 23 - Classes & Objects puts Calling the class makes an instance, Two accounts, two balances, Each instance owns its attributes. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

32. What has to happen first: Two counters count separately

Ranking

Put in order

Put the moves of Two counters count separately into the order they have to happen.

  1. __init__ takes no extra inputs here
  2. a is bumped twice, b once
  3. Trace each object's count

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. increment edits only the object it was called on.

33. Two counters count separately

Worked example

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1

a = Counter()
b = Counter()
a.increment()
a.increment()
b.increment()
print(a.count)
print(b.count)

__init__ takes no extra inputs here

Why: Every Counter starts at count = 0.

a is bumped twice, b once

Why: increment edits only the object it was called on.

Trace each object's count

Why: Verified by execution: prints 2 then 1.

stepa.countb.count
start00
a.increment()10
a.increment()20
b.increment()21

34. Watch it run: Two counters count separately

Pattern

Step through it

Step through Two counters count separately one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is start
  2. Step 2: step is a.increment()
  3. Step 3: step is a.increment()
  4. Step 4: step is b.increment()

35. One cookie cutter, many cookies

Intuition

The class is the cookie cutter; each instance is a cookie. The cutter decides the shape (the attributes and methods every cookie has).

But once cut, each cookie is its own object. Decorate one and the others are untouched - just like editing a.count leaves b.count alone.

36. The Dot

Section

Part 4

37. obj.attr reaches a stored value

Concept

The dot in a.balance means 'the balance attribute belonging to a'. Read it in an expression, or assign to it to change that object's state.

a.balance on the right of = reads; a.balance = ... on the left writes.

38. Predict the next row: Read an attribute, then update it

Pattern

Predict first

The table runs: start | 100 · a.balance + 25 | 125

In Read an attribute, then update it, given the rows so far: what is the next one — the row where step is after assign?

Correct: after assign | 125

stepa.balance
start100
a.balance + 25125
after assign125

Why: The relationship between the columns, not the individual numbers, is what generates the next row. a.balance is 100, so a.balance + 25 is 125.

39. Read an attribute, then update it

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
a.balance = a.balance + 25
print(a.balance)

The right side reads the old balance

Why: a.balance is 100, so a.balance + 25 is 125.

The left side writes the new one

Why: Verified by execution: a.balance becomes 125, and print shows 125.

stepa.balance
start100
a.balance + 25125
after assign125

40. Watch it run: Read an attribute, then update it

Pattern

Step through it

Step through Read an attribute, then update it one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is start
  2. Step 2: step is a.balance + 25
  3. Step 3: step is after assign

41. obj.method() runs a method

Concept

The same dot, followed by parentheses, calls a method: a.deposit(50) runs the deposit method on the object a.

Attribute or method? The parentheses tell them apart. a.balance is a value; a.deposit(50) is an action.

42. Two players, one scores

Worked example

class Player:
    def __init__(self, name):
        self.name = name
        self.score = 0
    def add(self, points):
        self.score = self.score + points

p1 = Player("Ana")
p2 = Player("Bo")
p1.add(10)
print(p1.score)
print(p2.score)

add() runs only on p1

Why: p1.add(10) edits p1.score; p2 was never touched.

Compare the two scores

Why: Verified by execution: prints 10 then 0.

objectscore
p110
p20

43. A list attribute grows over time

Worked example

class TodoList:
    def __init__(self):
        self.items = []
    def add(self, task):
        self.items.append(task)

t = TodoList()
t.add("wash")
t.add("cook")
print(t.items)
print(len(t.items))

self.items starts as an empty list

Why: Each add() appends to the same list living on the object.

Trace the two adds

Why: Verified by execution: prints ['wash', 'cook'] then 2.

stept.itemslen
start[]0
add("wash")['wash']1
add("cook")['wash', 'cook']2

44. Methods

Section

Part 5

45. A method is a function inside a class

Concept

Methods are just defs written inside the class body. They are the actions an instance can perform on its own data.

method — A function defined inside a class. Called on an instance with the dot, it can read and change that instance's attributes.

46. Term to definition: Session 23 - Classes & Objects

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. dict as a record
  • t2. class
  • t3. instance (object)
  • t4. method
  • d1. A dict groups named values (keys to values). It stores state, but any behavior lives in separate functions elsewhere.
  • d2. A blueprint that defines a new type: what data each instance holds (attributes) and what it can do (methods).
  • d3. One concrete thing built from a class. Each instance has its own copy of the attributes __init__ set on it.
  • d4. A function defined inside a class. Called on an instance with the dot, it can read and change that instance's attributes.

Why: These are the working definitions of dict as a record, class, instance (object), method as Session 23 - Classes & Objects uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.

47. Attributes are nouns, methods are verbs

Intuition

An object's attributes are what it has - a name, a balance, a list of tasks. Its methods are what it does - deposit, increment, add.

When you design a class, ask two questions: what does this thing hold (attributes on self), and what can it do (methods)?

48. The first parameter is always self

Concept

Every method's first parameter is self - the instance the method was called on. You write it in the definition; you do not pass it at the call.

Inside the method, reach the object's data through self: self.balance, self.name, and so on.

49. deposit changes the object's state

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    def deposit(self, amount):
        self.balance = self.balance + amount

a = Account("Ana", 100)
a.deposit(50)
print(a.balance)

deposit takes self and amount

Why: a.deposit(50) fills self = a and amount = 50.

It edits self.balance in place

Why: Verified by execution: 100 + 50 = 150, and print shows 150.

stepamounta.balance
start-100
a.deposit(50)50150

50. Restore the missing line: A Counter that increments

Fill the middle

Fill in the blanks

From A Counter that increments — one line has had its right-hand side removed. Put it back.

class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count = self.count + 1

c = Counter()
c.increment()
c.increment()
print(c.count)

Why: c is what everything below it consumes, so the wrong expression here fails later and somewhere else. increment reads the current count and stores one more.

51. A Counter that increments

Worked example

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count = self.count + 1

c = Counter()
c.increment()
c.increment()
print(c.count)

Each call bumps self.count by one

Why: increment reads the current count and stores one more.

Trace the two calls

Why: Verified by execution: prints 2.

stepc.count
start0
c.increment()1
c.increment()2

52. Watch it run: A Counter that increments

Pattern

Step through it

Step through A Counter that increments one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is start
  2. Step 2: step is c.increment()
  3. Step 3: step is c.increment()

53. Methods can return a value

Concept

A method can return just like any function. Some methods change state and return nothing; others compute an answer and hand it back.

A method that returns reads naturally at the call site: label = a.summary().

54. summary returns a string

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    def summary(self):
        return self.owner + " has " + str(self.balance)

a = Account("Ana", 100)
print(a.summary())

summary reads two attributes off self

Why: It joins self.owner and self.balance into one string.

str() makes the number joinable

Why: Verified by execution: prints Ana has 100.

expressionvalue
a.summary()Ana has 100

55. Finish it with less help: area computes from two attributes

Faded example

Fill in the blanks

area computes from two attributes, with the scaffolding fading: two lines are gone now — fill both.

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height

r = Rectangle(3, 4)
print(r.area())

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. Verified by execution: 3 * 4 = 12, and print shows 12.

56. area computes from two attributes

Worked example

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height

r = Rectangle(3, 4)
print(r.area())

area reads both attributes off self

Why: self.width is 3 and self.height is 4.

The method returns the product

Why: Verified by execution: 3 * 4 = 12, and print shows 12.

expressionvalue
r.width3
r.height4
r.area()12

57. Fill in: value for area computes from two attributes

Comparison

Comparison matrix

From area computes from two attributes: refill the value column from what you know. The rest of the table is as it appeared.

expressionvalue
r.width3
r.height4
r.area()12

58. Predict the next row: withdraw, called twice

Pattern

Predict first

The table runs: start | - | 100 · a.withdraw(30) | 30 | 70

In withdraw, called twice, given the rows so far: what is the next one — the row where step is a.withdraw(20)?

Correct: a.withdraw(20) | 20 | 50

stepamounta.balance
start-100
a.withdraw(30)3070
a.withdraw(20)2050

Why: The relationship between the columns, not the individual numbers, is what generates the next row. State carries over between calls because it lives on the object.

59. withdraw, called twice

Worked example

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    def withdraw(self, amount):
        self.balance = self.balance - amount

a = Account("Ana", 100)
a.withdraw(30)
a.withdraw(20)
print(a.balance)

Each call subtracts from the running balance

Why: State carries over between calls because it lives on the object.

Trace both withdrawals

Why: Verified by execution: 100 - 30 - 20 = 50, and print shows 50.

stepamounta.balance
start-100
a.withdraw(30)3070
a.withdraw(20)2050

60. Watch it run: withdraw, called twice

Pattern

Step through it

Step through withdraw, called twice one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is start
  2. Step 2: step is a.withdraw(30)
  3. Step 3: step is a.withdraw(20)

61. What self Really Is

Section

Part 6

62. self is the instance, passed automatically

Concept

self is not a keyword and not magic - it is simply the first argument, and Python fills it with the object you called the method on.

Call a.deposit(50) and Python quietly passes a in as self. You supply the rest of the arguments.

63. Proof: self is the object

Worked example

class Counter:
    def __init__(self):
        self.count = 0
    def show(self):
        print(self is c)

c = Counter()
c.show()

Inside show, compare self to c

Why: self is c asks whether they are the very same object.

Read the output

Why: Verified by execution: prints True - self really is the instance c.

expressionvalue
self is cTrue

64. a.deposit(50) means Account.deposit(a, 50)

Concept

The dot call is shorthand. a.deposit(50) is Python's way of writing Account.deposit(a, 50) - the object slides in as the first argument, self.

That is why every method lists self first: something has to catch the object being passed in.

65. Restore the missing line: self.name reaches this object's data

Fill the middle

Fill in the blanks

From self.name reaches this object's data — one line has had its right-hand side removed. Put it back.

class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " says woof"

d = Dog("Rex")
print(d.speak())

Why: d is what everything below it consumes, so the wrong expression here fails later and somewhere else. But self is there, filled with d, so self.name is "Rex".

66. self.name reaches this object's data

Worked example

class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " says woof"

d = Dog("Rex")
print(d.speak())

speak has no visible arguments

Why: But self is there, filled with d, so self.name is "Rex".

Read the output

Why: Verified by execution: prints Rex says woof.

expressionvalue
self.nameRex
d.speak()Rex says woof

67. Draw the shape of it: self.name reaches this object's data

Blank canvas

Draw it

Draw what self.name reaches this object's data just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

68. Traps

Section

Part 7

69. Something is wrong here: forgetting self in the signature

Anomaly

Predict first

A student writes this, and it looks reasonable:

The method is defined without self.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Python hands c to increment, but the method has no parameter to catch it - so it crashes with the traceback below.

List self as the first parameter.

Why: Python hands c to increment, but the method has no parameter to catch it - so it crashes with the traceback below.

70. Trap: forgetting self in the signature

Trap

The trap

The method is defined without self.

class Counter:
    def __init__(self):
        self.count = 0
    def increment():
        self.count = self.count + 1

c = Counter()
c.increment()

c.increment() still passes the object in

Why: Python hands c to increment, but the method has no parameter to catch it - so it crashes with the traceback below.

stepresult
def increment():0 parameters
c.increment()passes 1 argument (c)
outcomeTypeError

The fix

List self as the first parameter.

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count = self.count + 1

c = Counter()
c.increment()
print(c.count)

self catches the object

Why: The exact error from the wrong version: TypeError: Counter.increment() takes 0 positional arguments but 1 was given. With self added, it runs. Real output: 1.

stepc.count
start0
c.increment()1

71. The 'argument count' includes self

Concept

That message - 'takes 0 positional arguments but 1 was given' - almost always means you dropped self. The '1 given' is the invisible instance.

Fix: add self as the first parameter of the method.

72. Teach it back: The 'argument count' includes self

Explain it

Discussion prompt

Explain The 'argument count' includes self to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Fix: add self as the first parameter of the method.

73. Something is wrong here: reading an attribute that was never set

Anomaly

Predict first

A student writes this, and it looks reasonable:

A typo asks for an attribute the object does not have.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Reading a missing attribute is an error, not a blank - Python raises AttributeError and even suggests the name you meant.

Use the name exactly as set in __init__.

Why: Reading a missing attribute is an error, not a blank - Python raises AttributeError and even suggests the name you meant.

74. Trap: reading an attribute that was never set

Trap

The trap

A typo asks for an attribute the object does not have.

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(a.blance)

a has owner and balance, not blance

Why: Reading a missing attribute is an error, not a blank - Python raises AttributeError and even suggests the name you meant.

attributeon a?
a.owneryes
a.balanceyes
a.blanceno -> AttributeError

The fix

Use the name exactly as set in __init__.

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(a.balance)

Spelling matches the attribute

Why: The exact error from the wrong version: AttributeError: 'Account' object has no attribute 'blance'. Did you mean: 'balance'? Fixing the spelling prints 100.

you writeprints
a.balance100

75. Break it on purpose: reading an attribute that was never set

Break the constraint

Discussion prompt

The rule this trap just fixed:

The exact error from the wrong version: AttributeError: 'Account' object has no attribute 'blance'. Fixing the spelling prints 100.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

Reading a missing attribute is an error, not a blank - Python raises AttributeError and even suggests the name you meant.

76. Set an attribute before you read it

Concept

An attribute exists only after something assigns to it - usually self.x = ... in __init__. Read one that was never set and you get the same AttributeError.

So a misspelling and a genuinely missing attribute report the same way. Check __init__ first.

77. By analogy: Set an attribute before you read it

Analogy

Discussion prompt

Explain Set an attribute before you read it by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

An attribute exists only after something assigns to it - usually self.x = ... in __init__. Read one that was never set and you get the same AttributeError.

78. Something is wrong here: a method without parentheses

Anomaly

Predict first

A student writes this, and it looks reasonable:

The parentheses are missing, so nothing runs.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Without (), you get the method object itself - a 'bound method' - not the result of running it.

Add () to actually call the method.

Why: Without (), you get the method object itself - a 'bound method' - not the result of running it. Nothing is incremented.

79. Trap: a method without parentheses

Trap

The trap

The parentheses are missing, so nothing runs.

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1

c = Counter()
print(c.increment)

c.increment names the method but does not call it

Why: Without (), you get the method object itself - a 'bound method' - not the result of running it. Nothing is incremented.

you writeprints
c.increment<bound method Counter.increment of <...Counter object...>>

The fix

Add () to actually call the method.

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1

c = Counter()
c.increment()
print(c.count)

c.increment() runs the method

Why: The parentheses trigger the call, so count goes up. Real output: 1. The exact wrong-version print was <bound method Counter.increment of <__main__.Counter object at 0x...>>.

stepc.count
start0
c.increment()1

80. Inspect it line by line: Trap: a method without parentheses

Error analysis

Annotate

Walk the callouts on Trap: a method without parentheses. Each one is a place this is easy to get subtly wrong.

  • Without (), you get the method object itself - a 'bound method' - not the result of running it. Nothing is incremented.
  • The parentheses trigger the call, so count goes up. Real output: 1. The exact wrong-version print was <bound method Counter.increment of <__main__.Counter object at 0x...>>.

81. A bound method is a value, not a call

Concept

c.increment is a real object - the method already tied ('bound') to c. Printing it shows <bound method ...>. It just has not been run yet.

Whenever you see <bound method ...> in your output, you forgot the ().

82. Break it if you can: A bound method is a value, not a call

Counterexample

Discussion prompt

c.increment is a real object - the method already tied ('bound') to c. Printing it shows <bound method ...>. It just has not been run yet.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Whenever you see <bound method ...> in your output, you forgot the ().

83. Something is wrong here: missing an __init__ argument

Anomaly

Predict first

A student writes this, and it looks reasonable:

__init__ needs two values; the call passes one.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Creating the object runs __init__, and it is short one argument - Python raises a TypeError naming the one you left out.

Pass every parameter __init__ declares (after self).

Why: Creating the object runs __init__, and it is short one argument - Python raises a TypeError naming the one you left out.

84. Trap: missing an __init__ argument

Trap

The trap

__init__ needs two values; the call passes one.

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana")

owner is filled, balance is not

Why: Creating the object runs __init__, and it is short one argument - Python raises a TypeError naming the one you left out.

parametergiven?
owneryes ("Ana")
balanceno -> TypeError

The fix

Pass every parameter __init__ declares (after self).

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(a.balance)

Both owner and balance are supplied

Why: The exact error from the wrong version: TypeError: Account.__init__() missing 1 required positional argument: 'balance'. With both given, it prints 100.

you writeprints
Account("Ana", 100)100

85. Which of these survive contact with Session 23 - Classes & Objects?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
You have used a dict to keep related facts under one name - an account's owner and balance, say. It holds data, but it does nothing on its own.; To deposit money you write a separate function that takes the dict, edits account["balance"], and hopes every caller spells the keys right.; Think of a class as a blueprint for a kind of thing - an Account, a Counter, a Dog. It describes what every one of them has and what every one of them can do.
Breaks
The method is defined without self.; A typo asks for an attribute the object does not have.
sound
These are stated as this lesson states them — each one survives the edge cases Session 23 - Classes & Objects puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

86. Patterns & Checks

Section

Part 8

87. Writing a class

Pattern

1. class Name: opens the blueprint

Why: Everything indented under it belongs to the class.

2. def __init__(self, ...): sets up each instance

Why: Store the inputs as attributes: self.x = x. It runs automatically at creation.

3. Add methods, each starting with self

Why: Methods read and change the object's own attributes through self.

4. Create with Name(args); use with the dot

Why: obj.attr reads a value; obj.method() runs an action.

88. Reading the three classic errors

Pattern

'takes 0 positional arguments but 1 was given' -> add self

Why: The method signature is missing self; the '1 given' is the instance.

'object has no attribute ...' -> check the name

Why: A typo, or an attribute never set in __init__. Match the spelling exactly.

Output shows '<bound method ...>' -> add ()

Why: You named the method instead of calling it. Parentheses run it.

89. Where this shows up: Session 23 - Classes & Objects

Real world

Discussion prompt

Outside this lesson: where does Session 23 - Classes & Objects actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Reading the three classic errors is doing the work in it.

Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.

Answer:

Session 23 of the Python Fundamentals series, in depth. Moving from a dict that only holds data to a class that bundles data and behavior together: the class keyword, __init__, self, instance attributes, creating instances, and the fact that each instance carries its own state.

90. Check: reading an attribute

Check

What comes out?

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

a = Account("Ana", 100)
print(a.owner)
expressionvalue
a.owner?

Check your understanding

What does this print?

  • A. Ana (correct)
  • B. owner
  • C. 100
  • D. <class 'Account'>

Answer: A

Why: __init__ set self.owner = owner, so a.owner is the value passed in, "Ana". Verified by execution.

Why B tempts people
owner is the attribute name, not its value; a.owner reads the stored value "Ana".
Why C tempts people
100 is a.balance, not a.owner. The two attributes hold different values.
Why D tempts people
That is what type(a) shows; a.owner reads the owner attribute instead.

91. Check: separate state

Check

Only one player scores.

class Player:
    def __init__(self, name):
        self.name = name
        self.score = 0
    def add(self, points):
        self.score += points

p1 = Player("Ana")
p2 = Player("Bo")
p1.add(10)
print(p2.score)
objectscore
p110
p2?

Check your understanding

What does print(p2.score) show?

  • A. 0 (correct)
  • B. 10
  • C. None
  • D. Error

Answer: A

Why: add() ran only on p1, so p1.score is 10 while p2.score is still its starting 0. Each instance owns its own state. Verified by execution.

Why B tempts people
10 is p1.score. p2 never had add() called on it, so it is unchanged.
Why C tempts people
score was set to 0 in __init__, so it holds 0, not None.
Why D tempts people
score exists (set in __init__), so reading it is fine - no error.

92. What each one costs: Check: separate state

Trade off

Comparison matrix

From Check: separate state: every row here is a choice with a cost. Fill the score column, then say which row you would actually pick and what you give up for it.

objectscore
p110
p2?

93. Check: a method that updates

Check

Trace the deposits.

class Account:
    def __init__(self, balance):
        self.balance = balance
    def deposit(self, amount):
        self.balance += amount

a = Account(100)
a.deposit(20)
a.deposit(30)
print(a.balance)
stepa.balance
start100
+20120
+30?

Check your understanding

What does this print?

  • A. 150 (correct)
  • B. 100
  • C. 50
  • D. 30

Answer: A

Why: Each deposit adds to the running balance stored on the object: 100 + 20 + 30 = 150. Verified by execution.

Why B tempts people
100 is the starting balance; the two deposits do change it because state persists on the object.
Why C tempts people
50 is 20 + 30 - but deposits are added to the starting 100, not just to each other.
Why D tempts people
30 is only the last deposit; the earlier balance is not discarded between calls.

94. Fill in: a.balance for Check: a method that updates

Comparison

Comparison matrix

From Check: a method that updates: refill the a.balance column from what you know. The rest of the table is as it appeared.

stepa.balance
start100
+20120
+30?

95. Check: the missing-self error

Check

The method has no self.

class Counter:
    def __init__(self):
        self.count = 0
    def increment():
        self.count += 1

c = Counter()
c.increment()
def incrementcallresult
()c.increment()?

Check your understanding

What happens when c.increment() runs?

  • A. TypeError: increment() takes 0 positional arguments but 1 was given (correct)
  • B. It works; count becomes 1
  • C. NameError: self is not defined
  • D. AttributeError: no attribute 'increment'

Answer: A

Why: The call passes the instance c as the first argument, but increment() declares no parameter to receive it, so Python raises TypeError about positional arguments. Verified by execution.

Why B tempts people
It does not work - without a self parameter there is nowhere for the passed-in instance to go.
Why C tempts people
The error is a TypeError raised at the call, before the body runs - not a NameError about self.
Why D tempts people
increment does exist on the class; the problem is its parameter list, not a missing attribute.

96. Check: forgotten parentheses

Check

Spot the missing call.

class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " says woof"

d = Dog("Rex")
print(d.speak)
you writeprints
d.speak?

Check your understanding

What does print(d.speak) show?

  • A. A <bound method ...> object (correct)
  • B. Rex says woof
  • C. Rex
  • D. AttributeError

Answer: A

Why: Without parentheses, d.speak is the method object itself, so print shows <bound method Dog.speak of ...>. Adding () would run it and print Rex says woof. Verified by execution.

Why B tempts people
That is the return value of d.speak() - with the parentheses. Here they are missing, so nothing runs.
Why C tempts people
Rex is d.name; d.speak refers to the method, not the name attribute.
Why D tempts people
speak exists on the object, so there is no AttributeError - you just get the uncalled method.

97. Check: what self is

Check

Think about the first parameter.

class Counter:
    def __init__(self):
        self.count = 0
    def show(self):
        print(self is c)

c = Counter()
c.show()
expressionvalue
self is c?

Check your understanding

What does c.show() print?

  • A. True (correct)
  • B. False
  • C. None
  • D. Error - you can't compare self

Answer: A

Why: When you call c.show(), Python passes c in as self, so self and c are the very same object and self is c is True. Verified by execution.

Why B tempts people
self is not a copy - it is the exact instance you called the method on, so the identity check is True.
Why C tempts people
print reports the value of self is c, which is the boolean True, not None.
Why D tempts people
Comparing self with is is perfectly valid; it just asks whether they are the same object.

98. Check: a returning method

Check

The method returns a value.

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height

r = Rectangle(3, 4)
print(r.area())
widthheightarea()
34?

Check your understanding

What does print(r.area()) show?

  • A. 12 (correct)
  • B. 7
  • C. <bound method Rectangle.area ...>
  • D. None

Answer: A

Why: area() reads self.width (3) and self.height (4) and returns their product, 3 * 4 = 12. Verified by execution.

Why B tempts people
7 is 3 + 4; area multiplies the two attributes, it does not add them.
Why C tempts people
That would print if you wrote r.area without (); here the parentheses call it, so you get the returned value.
Why D tempts people
area has a return statement, so it hands back 12 rather than the default None.

99. Connect it up: Session 23 - Classes & Objects

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — From Dicts to Classes · class and __init__ · Instances & Their Own State · The Dot · Methods · What self Really Is. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

100. What you can do now

Recap

A class is a blueprint that bundles data and behavior. __init__ sets up each instance's attributes on self; methods act on that state; and every instance keeps its own.

You writeIt means
class Account:define a new type
def __init__(self, owner):set up each new instance
self.owner = ownerstore an attribute on the object
a = Account("Ana")create one instance
a.balanceread that object's attribute
a.deposit(50)run a method on that object

And the three tells: 'takes 0 positional arguments' means add self; 'no attribute' means check the name; <bound method ...> means add (). Next session we give objects a friendly __str__ and let one class build on another.

Sources

  1. Python 3 Tutorial - Classes
  2. Python 3 Tutorial - Class Objects and Instance Objects
  3. Python 3 Reference - The __init__ method
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 23).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108