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
Title
Python Fundamentals - Session 23
Bundle data and behavior into one named thing
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:
__init__, and store instance attributes on self.obj.attr to read a value, obj.method() to run a method.self is - the instance, passed in automatically.self, missing attribute, and a method used without ().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.
Section
Part 1
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.
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.
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.
| expression | value |
|---|---|
| account["owner"] | Ana |
| account["balance"] | 100 |
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.
| expression | value |
|---|---|
| account["owner"] | Ana |
| account["balance"] | 100 |
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.
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.
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.
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.
Section
Part 2
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).
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.
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(...).
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.
Ranking
Put in order
Put the moves of Define the Account class into the order they have to happen.
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.
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.
| expression | value |
|---|---|
| a.owner | Ana |
| a.balance | 100 |
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.
| expression | value |
|---|---|
| a.owner | Ana |
| a.balance | 100 |
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.
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.
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.
| expression | output |
|---|---|
| type(a) | <class '__main__.Account'> |
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.
Section
Part 3
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.
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.
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.
| object | owner | balance |
|---|---|---|
| a | Ana | 100 |
| b | Bo | 5 |
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.
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.
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.
Ranking
Put in order
Put the moves of Two counters count separately into the order they have to happen.
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.
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.
| step | a.count | b.count |
|---|---|---|
| start | 0 | 0 |
| a.increment() | 1 | 0 |
| a.increment() | 2 | 0 |
| b.increment() | 2 | 1 |
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?
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.
Section
Part 4
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.
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
| step | a.balance |
|---|---|
| start | 100 |
| a.balance + 25 | 125 |
| after assign | 125 |
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.
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.
| step | a.balance |
|---|---|
| start | 100 |
| a.balance + 25 | 125 |
| after assign | 125 |
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?
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.
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.
| object | score |
|---|---|
| p1 | 10 |
| p2 | 0 |
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.
| step | t.items | len |
|---|---|---|
| start | [] | 0 |
| add("wash") | ['wash'] | 1 |
| add("cook") | ['wash', 'cook'] | 2 |
Section
Part 5
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.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
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.
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)?
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.
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.
| step | amount | a.balance |
|---|---|---|
| start | - | 100 |
| a.deposit(50) | 50 | 150 |
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.
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.
| step | c.count |
|---|---|
| start | 0 |
| c.increment() | 1 |
| c.increment() | 2 |
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?
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().
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.
| expression | value |
|---|---|
| a.summary() | Ana has 100 |
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.
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.
| expression | value |
|---|---|
| r.width | 3 |
| r.height | 4 |
| r.area() | 12 |
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.
| expression | value |
|---|---|
| r.width | 3 |
| r.height | 4 |
| r.area() | 12 |
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
| step | amount | a.balance |
|---|---|---|
| start | - | 100 |
| a.withdraw(30) | 30 | 70 |
| a.withdraw(20) | 20 | 50 |
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.
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.
| step | amount | a.balance |
|---|---|---|
| start | - | 100 |
| a.withdraw(30) | 30 | 70 |
| a.withdraw(20) | 20 | 50 |
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?
Section
Part 6
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.
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.
| expression | value |
|---|---|
| self is c | True |
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.
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".
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.
| expression | value |
|---|---|
| self.name | Rex |
| d.speak() | Rex says woof |
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.
Section
Part 7
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.
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.
| step | result |
|---|---|
| def increment(): | 0 parameters |
| c.increment() | passes 1 argument (c) |
| outcome | TypeError |
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.
| step | c.count |
|---|---|
| start | 0 |
| c.increment() | 1 |
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.
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.
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.
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.
| attribute | on a? |
|---|---|
| a.owner | yes |
| a.balance | yes |
| a.blance | no -> AttributeError |
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 write | prints |
|---|---|
| a.balance | 100 |
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.
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.
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.
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.
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 write | prints |
|---|---|
| c.increment | <bound method Counter.increment of <...Counter object...>> |
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...>>.
| step | c.count |
|---|---|
| start | 0 |
| c.increment() | 1 |
Error analysis
Annotate
Walk the callouts on Trap: a method without parentheses. Each one is a place this is easy to get subtly wrong.
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 ().
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 ().
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.
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.
| parameter | given? |
|---|---|
| owner | yes ("Ana") |
| balance | no -> TypeError |
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 write | prints |
|---|---|
| Account("Ana", 100) | 100 |
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.
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.self.; A typo asks for an attribute the object does not have.Section
Part 8
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.
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.
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.
Check
What comes out?
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
a = Account("Ana", 100)
print(a.owner)| expression | value |
|---|---|
| a.owner | ? |
Check your understanding
What does this print?
Answer: A
Why: __init__ set self.owner = owner, so a.owner is the value passed in, "Ana". Verified by execution.
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)| object | score |
|---|---|
| p1 | 10 |
| p2 | ? |
Check your understanding
What does print(p2.score) show?
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.
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.
| object | score |
|---|---|
| p1 | 10 |
| p2 | ? |
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)| step | a.balance |
|---|---|
| start | 100 |
| +20 | 120 |
| +30 | ? |
Check your understanding
What does this print?
Answer: A
Why: Each deposit adds to the running balance stored on the object: 100 + 20 + 30 = 150. Verified by execution.
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.
| step | a.balance |
|---|---|
| start | 100 |
| +20 | 120 |
| +30 | ? |
Check
The method has no self.
class Counter:
def __init__(self):
self.count = 0
def increment():
self.count += 1
c = Counter()
c.increment()| def increment | call | result |
|---|---|---|
| () | c.increment() | ? |
Check your understanding
What happens when c.increment() runs?
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.
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 write | prints |
|---|---|
| d.speak | ? |
Check your understanding
What does print(d.speak) show?
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.
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()| expression | value |
|---|---|
| self is c | ? |
Check your understanding
What does c.show() print?
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.
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())| width | height | area() |
|---|---|---|
| 3 | 4 | ? |
Check your understanding
What does print(r.area()) show?
Answer: A
Why: area() reads self.width (3) and self.height (4) and returns their product, 3 * 4 = 12. Verified by execution.
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.
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 write | It means |
|---|---|
| class Account: | define a new type |
| def __init__(self, owner): | set up each new instance |
| self.owner = owner | store an attribute on the object |
| a = Account("Ana") | create one instance |
| a.balance | read 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.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.