On this page
- In this lesson
- Chapter 1: Why We Need OOP — Procedural Code Versus Objects
- Chapter 2: Class and Object — Defining a Template and Creating Objects from It
- Chapter 3:
__init__andself— an Object That Is Born Complete - Chapter 4: Methods — the Object’s Actions
- Chapter 5: Shared Data — Instance Attribute Versus Class Attribute
- Chapter 6: What Becomes a Class — From a Verbal Requirement to Classes
- Chapter 7: Back to the Bank — Everything Together
- Practice
- Mini project: A small library
Classes and Objects: From the Problem to a Complete Class
- Recognize the problems of procedural code as the system grows, and explain how OOP solves them
- Define a Class and create several objects from it, and tell a Class apart from an Object
- Write an
__init__that initializes a complete object and checks the values, and explain whatselfis - Add Methods — Getters and Setters — and a
__str__that displays the object - Tell apart an Instance Attribute and a Class Attribute, and use a shared counter
- Turn a verbal description into a class structure — what is a Class, what is an Attribute and what is a Method
- Build a complete Class that connects all the ideas — in a mini project
In this lesson
This lesson is built as one story, in the same order as the slides. We start from a small bank system written with dictionaries and functions, see where it breaks, and in every chapter add one tool that solves one problem:
- Chapter 1 — why we need OOP: the problems of procedural code
- Chapter 2 — Class and Object: a template and instances
- Chapter 3 —
__init__andself: an object that is born complete - Chapter 4 — Methods: the object does not only know, it also does
- Chapter 5 — shared data: Class Attributes
- Chapter 6 — what becomes a Class: from a verbal requirement to classes
- Chapter 7 — back to the bank: everything together
At the end of the lesson — practice, and a mini project that connects all the chapters.
Chapter 1: Why We Need OOP — Procedural Code Versus Objects
What will we learn?
This whole part of the course is dedicated to OOP — Object-Oriented Programming. Before we write a single line of OOP code, we will answer the most important question: why?
In this chapter:
- We will write a small system without OOP, in the way we already know.
- We will see where it starts to become problematic.
- We will understand the idea of OOP as the solution, still without syntax.
Why do we need this?
Everything we learned until now works. You can write any program with variables, dicts, lists and functions. This is called Procedural Programming: data on one side, functions that act on it on the other.
But when a system grows, to dozens of kinds of “things” and hundreds of functions, problems start. OOP is the way almost all large systems are built. And the tools you will use (FastAPI, Python libraries) are written in OOP too. To read them and use them, you need to understand it.
Explanation from the basics
The problem: a bank account system — without OOP
Let’s build a small system for managing bank accounts, with what we already know:
account1 = {"owner": "Dana", "balance": 1000}
account2 = {"owner": "Avi", "balance": 500}
def deposit(account, amount):
if amount <= 0:
raise ValueError("amount must be positive")
account["balance"] = account["balance"] + amount
def withdraw(account, amount):
if amount > account["balance"]:
raise ValueError("insufficient funds")
account["balance"] = account["balance"] - amount
deposit(account1, 200)
withdraw(account2, 100)
print(account1, account2){'owner': 'Dana', 'balance': 1200} {'owner': 'Avi', 'balance': 400}It works! So what is the problem? Let’s make the system bigger.
Problem 1: anyone can “touch” the data
account1["balance"] = -999999
account2["balnce"] = 100- The first line bypasses all the checks we wrote in
withdraw. A negative balance, without any check. - The second line has a typo. A new key
"balnce"was created, and there is no error at all (we saw this in the data structures and functions topic).
The rules (don’t withdraw more than there is, a positive amount) exist only if everyone remembers to use the functions. Nothing forces them.
Problem 2: the data and the actions are scattered
Every action on an account is a separate function that receives the dict: deposit(account, ...), withdraw(account, ...), transfer(...), monthly_fee(...), print_statement(...). In a real system, these are dozens of functions in several files. Whoever receives an account dict does not know which actions exist on it and where they are.
Problem 3: there is no defined “structure”
What does an account look like? What must be in it? Maybe in one place in the code someone created {"owner": ..., "balance": ...}, and in another place {"name": ..., "money": ...}. There is no one place that defines “this is a bank account, these are its fields, and these are its actions”.
Problem 4: similar but different kinds
A “savings account” was added: like a regular account, but with interest, and without the option to withdraw below 1,000. Now withdraw has to check: if account["type"] == "savings": ... elif .... And with every new kind of account, all the functions fill up with ifs.
The idea of OOP
OOP organizes the code around “things” (objects), and not around actions. Every “thing” contains together:
- Its data: Attributes. For an account: the owner, the balance.
- The actions you can do with it: Methods. For an account: deposit, withdraw.
PROCEDURAL OBJECT-ORIENTED
data: account dict ┌──────────────────────────┐
functions: deposit(account, x) │ BankAccount │
withdraw(account, x) │ data: owner, balance│
print_statement(account) │ actions: deposit() │
│ withdraw() │
"functions act ON data" │ statement() │
└──────────────────────────┘
"the object knows its own data
and its own actions"And how does it solve the problems?
| Problem | The OOP solution | We will learn it in… |
|---|---|---|
| Anyone touches the data | The object protects its data, and changes go through its actions (Encapsulation) | The next lesson |
| Data and actions are scattered | Everything is inside the Class. account. + Tab in VS Code shows everything you can do | Today |
| No defined structure | One Class defines exactly which data every account has | Today |
| Similar kinds | Inheritance and Polymorphism: a savings account “inherits” from a regular account and changes only what is different | The inheritance and polymorphism lessons |
A template versus a specific thing
The most basic idea of OOP already exists in the real world:
| Template (Class) | Specific things created from it (Objects) |
|---|---|
| The building plan of an apartment | Dana’s apartment on floor 3, Avi’s apartment on floor 5 |
| The “student details” form | The form Dana filled in, the form Avi filled in |
| A template for producing cars on a production line | The red car with license plate 12-345-67 |
| The concept “bank account” | Dana’s account with 1,200 NIS, Avi’s account with 400 NIS |
- All the apartments built from the same plan have the same structure (rooms, kitchen), but different values (wall color, who lives there).
- A change to Dana’s apartment does not change Avi’s apartment.
- The plan itself is not an apartment. You cannot live in a plan.
Class = a template for producing cars. It defines that every car has a color, a model and a license plate (Attributes), and that every car knows how to drive, brake and honk (Methods). Object = one specific car produced from the template: red, model 2024, plate 12-345-67. You can produce thousands of cars from it, each with its own color and number.
Syntax
We don’t write a class yet. In the next chapter we will learn the full syntax. But here is a “taste”, to see where we are going. The same bank system, in OOP:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance = self.balance - amount
account1 = BankAccount("Dana", 1000)
account1.deposit(200)
print(account1.balance)Don’t try to understand every word now (class, __init__, self), because each of them has a whole chapter later in the lesson. Just notice one thing: the data (owner, balance) and the actions (deposit, withdraw) are in the same place, and you call them through the object: account1.deposit(200).
First code example
You have been using objects all the time, without knowing it! In Python, every value is an object, with its own data and methods:
name = "dana cohen"
print(name.title())
print(name.upper())
print(type(name))
numbers = [3, 1, 2]
numbers.append(5)
numbers.sort()
print(numbers)
print(type(numbers))Output:
Dana Cohen
DANA COHEN
<class 'str'>
[1, 2, 3, 5]
<class 'list'>Breaking down the code line by line
| Code | What it is in OOP |
|---|---|
name = "dana cohen" | An object of type str was created. Its data: the characters. |
name.title() | A call to the object’s method title. It “knows” how to work on its own text. |
type(name) → <class 'str'> | The object’s Class (template) is str. |
numbers.append(5) | A method of a list object that changes its own data. |
<class 'list'> | list is also a Class. Every list you created is an Object of the Class list. |
Key point: str and list are Classes that Python wrote for us. In this part we will learn to write Classes of our own: BankAccount, Student, Product, which suit our problem.
Another example
Another procedural problem: a student system. Take the following code, and try to spot the problems yourself.
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def add_grade(student, grade):
student["grades"].append(grade)
def average(student):
return sum(student["grades"]) / len(student["grades"])
add_grade(student1, 100)
student2["grades"].append(500)
print(average(student1))
print(average(student2))
print(average(student3))91.66666666666667
285.0
Traceback (most recent call last):
...
ZeroDivisionError: division by zeroThe problems:
student3was created with a typo ("nmae"). There is no place that defines what “must” be in a student.student2["grades"].append(500)bypassedadd_grade. Even ifadd_gradechecked that the grade is between 0 and 100, the check would not run. An average of 285!averagecrashes on a student without grades. And whoever uses a student dict needs to know that this function exists, and remember to handle this case.
In OOP, Student will be one Class with a defined structure (a name and grades, always), with a method add_grade that is the only way to add a grade (and checks it), and with a method average that knows how to handle an empty list. The rules sit next to the data they protect.
Summary
- Procedural programming: data (dicts) and functions separately. It works, but as the system grows, the data is exposed, the actions are scattered, and there is no binding structure.
- OOP: organize the code around things. Every object holds data (Attributes) and actions (Methods) together.
- Class = a template (a building plan, a production template). Object = a specific thing created from it.
- In Python every value is an object:
strandlistare Classes, and"hi".upper()is a call to a method. - OOP is a way of thinking: which things exist, what they know, and what they do. Not everything needs to be a Class.
Official documentation
Chapter 2: Class and Object — Defining a Template and Creating Objects from It
What will we learn?
- What a Class is and what an Object is (also called an Instance).
- What the word
classmeans. - The syntax for defining a Class.
- How to create an Object from a Class.
- Why every Object is independent.
Why do we need this?
In the previous chapter we saw that the problem with “dict + functions” is that there is no defined structure and no one place that gathers the data and the actions. A Class is the tool that creates this structure. Everything else (__init__, self, methods) is built on it.
Explanation from the basics
What is a Class?
A Class is a template (Blueprint) that defines a new kind of thing: what information every thing of this kind will have, and what it will be able to do.
- A Class does not contain the data of a specific thing. It is only the definition.
- From one Class you can create as many things as you want.
- When you define a Class, you are in fact creating a new type in Python, exactly like
strorlist.
What is an Object (Instance)?
An Object is a specific thing created from a Class. It has data of its own.
Instance is a synonym: “an Object of the Class Student” = “an Instance of Student”. The act of creating an Object from a Class is called Instantiation.
| Class | Object / Instance | |
|---|---|---|
| What it is | A template, a definition | A specific thing |
| How many | One for each kind | As many as you want |
| Data | Defines which fields there will be | Holds specific values |
| Example | Student | Dana, Avi, Noa |
| Analogy | A car production template | The red car with plate 12-345-67 |
The word class
class is a reserved word (Keyword) in Python, like def, if and for. It tells Python: “here begins a definition of a new kind”. After it comes the Class’s name, and after that an indented body.
Names of Classes
According to PEP 8, Class names are written in PascalCase: every word starts with a capital letter, without underscores. Student, BankAccount, ShoppingCart. This is different from functions and variables (snake_case), and that way you immediately see if something is a Class.
Syntax
class ClassName:
# class body - indented
pass
my_object = ClassName()| Part | Explanation |
|---|---|
class | A reserved word: “a Class definition begins”. |
ClassName | The Class’s name, in PascalCase. |
: | Required, like after def or if. |
| An indented body | What the template includes (in the next chapters: __init__ and methods). |
pass | “Do nothing”. The body of a Class (or a function) cannot be empty, so pass is a placeholder. |
ClassName() | Creating an Object: you call the Class with brackets, like a function. You get a new Object. |
First code example
week-3/first_class.py:
class Person:
pass
dana = Person()
avi = Person()
print(dana)
print(avi)
print(type(dana))
print(dana is avi)
print(isinstance(dana, Person))Output:
<__main__.Person object at 0x000001F4A3B2C1D0>
<__main__.Person object at 0x000001F4A3B2C2E0>
<class '__main__.Person'>
False
TrueBreaking down the code line by line
| Line | Explanation |
|---|---|
class Person: | Defines a new kind named Person. No person has been created yet. |
pass | The template is empty for now. |
dana = Person() | Instantiation: a new Object of type Person was created, and the name dana points to it. |
avi = Person() | Another Object, completely separate. |
print(dana) | The Object has no “content” to show, so Python prints a default description: the type, and an address in memory (0x...). Your address will be different. |
| Two different addresses | Two separate objects in RAM. |
type(dana) | <class '__main__.Person'>. The object’s Class is Person, which was defined in the main file (__main__). |
dana is avi | False: not the same object (remember is!). |
isinstance(dana, Person) | True: “is dana an Instance of Person?” |
Another example
Adding data to an object — and seeing why this is not the way. You can attach an Attribute to an object from outside, with a dot:
class Product:
pass
laptop = Product()
laptop.name = "Laptop"
laptop.price = 3500
mouse = Product()
mouse.name = "Mouse"
mouse.price = 80
print(laptop.name, laptop.price)
print(mouse.name, mouse.price)
mouse.price = 70
print("After update:", mouse.price, "| laptop unchanged:", laptop.price)Output:
Laptop 3500
Mouse 80
After update: 70 | laptop unchanged: 3500Breakdown:
laptop.name = "Laptop"— creates an Attribute namednameon the objectlaptop. The dot means “of”:laptop.name= “the name of the laptop”.- Every object has Attributes of its own. Changing
mouse.pricedoes not touchlaptop. - This is already better than a dict:
laptop.priceis more readable thanlaptop["price"].
But there is a big problem here. What guarantees that every Product has a name and a price?
keyboard = Product()
keyboard.nmae = "Keyboard"
print(keyboard.name)AttributeError: 'Product' object has no attribute 'name'- A typo (
nmae) created a different Attribute. - We forgot to define
price, and there is no warning until someone tries to use it. - This is exactly the problem of the dict from the previous chapter! The empty Class still does not define a structure.
The solution: the Class itself should define which data every object must have, and fill it in at the moment of creation. That is the role of __init__, the topic of the next chapter.
Summary
- Class = a template that defines a new kind. Object / Instance = a specific thing created from it.
- Syntax:
class Name:+ an indented body (orpass). Names in PascalCase. - Creating an Object:
obj = Name(), with brackets. Without brackets, it is the Class itself. - Every Object is independent:
a is b→False. The Attributes of one do not affect the other. type(obj)returns the Class.isinstance(obj, Name)checks membership.- Adding Attributes “from outside” works, but does not define a structure. That is why we need
__init__.
Official documentation
Chapter 3: __init__ and self — an Object That Is Born Complete
What will we learn?
- What
__init__is, the function that runs automatically when an object is created. - What a Constructor is.
- What
selfis, the word that appears everywhere in OOP. - Instance Attributes: the data of every object.
Why do we need this?
In the previous chapter we added Attributes “from outside”: laptop.name = "Laptop". We saw that this is dangerous: you can forget a field, make a typo, and there is no binding structure. We even wrote a helper function make_book to guarantee that every book has a title and pages.
__init__ is exactly that make_book, only inside the Class. It guarantees that every object is created initialized: with all its fields, and with values, from the very first moment.
Explanation from the basics
__init__ — the initialization method
__init__ (pronounced “dunder init”, double underscore init) is a special function defined inside a Class. It:
- Is called automatically the moment a new object is created:
Student(...). - Initializes the object, that is, puts the initial data into it.
- Returns nothing (
returnis not needed).
The name with two underscores on each side marks a special method of Python (called a Dunder Method or a Magic Method). Python looks for exactly this name, and calls it by itself. You never write obj.__init__() yourself.
Constructor
Constructor = the code that runs when a new object is built. In Python, __init__ plays this role, so it is usually called “the Constructor”. (To be technically precise: the building itself, allocating the object in memory, happens one step earlier, and __init__ “only” fills it in. For the purposes of the course, __init__ = Constructor.)
self — “this object”
self is the first parameter of every method in a Class, including __init__. It points to the specific object the method is working on right now.
Why is it needed? The Class is one template, but many objects are created from it. When the code in __init__ says “save the name”, it needs to know in which object to save it. self is the answer: “in the object being created now”.
self.name = nameRead it as: “in this object (self), create an Attribute named name, and put in it the value received in the parameter name”.
self.name— the object’s Attribute. It stays as long as the object exists.name(alone) — the parameter of__init__: a local variable (Local, remember Scope!) that disappears at the end of__init__.
Where does self come from? Python passes it automatically! When you write Student("Dana", 24), you pass 2 arguments, but __init__ receives 3: Python adds the new object as self, at the start.
The Class is an empty form with fields: “Name: ____ Age: ____”. __init__ is the registration clerk who fills in a new form for every student who arrives. self is “the form on the desk right now”: the clerk writes “Name: Dana” on this form, and not on all the forms in the cabinet. Tomorrow Avi will arrive, and a different form will be filled in, but the clerk will again call it “the form on the desk” (self).
What happens on the line dana = Student("Dana", 24)
- Python creates a new empty object of type
Studentin memory. - Python calls
__init__and passes it:self= the new object,name = "Dana",age = 24. __init__runsself.name = name, meaning thenameof the new object ="Dana". And the same forage.__init__ends. The local parameters disappear, and the Attributes stay on the object.- Python returns the object, and the name
danapoints to it.
Instance Attributes
Instance Attributes = data that belongs to a specific object. Every object has its own values. You define them in __init__ with self.something = ....
Syntax
class ClassName:
def __init__(self, param1, param2):
self.attribute1 = param1
self.attribute2 = param2
self.attribute3 = "default value" # attributes do not have to come from parameters
obj = ClassName(arg1, arg2) # Python calls __init__(obj, arg1, arg2) for you
obj.attribute1 # read an instance attribute__init__is written inside the Class, indented.selfis always first in the parameter list.- In the call, you do not pass
self. - You can give parameters default values, as in any function:
def __init__(self, name, age=18):.
First code example
week-3/student.py:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
self.grades = []
dana = Student("Dana", 24)
avi = Student("Avi", 31)
print(dana.name, dana.age, dana.grades)
print(avi.name, avi.age, avi.grades)
dana.grades.append(95)
print("Dana's grades:", dana.grades)
print("Avi's grades:", avi.grades)Output:
Dana 24 []
Avi 31 []
Dana's grades: [95]
Avi's grades: []Breaking down the code line by line
| Line | Explanation |
|---|---|
class Student: | Defining the template. |
def __init__(self, name, age): | The Constructor. self = the object being created. name, age = what you pass at creation. |
self.name = name | An Attribute name on the object, with the value from the parameter. |
self.age = age | The same for the age. |
self.grades = [] | An Attribute that does not come from a parameter: every student starts with an empty grade list of their own. |
dana = Student("Dana", 24) | Python: creates an object, calls __init__(new_obj, "Dana", 24), and returns it. |
avi = Student("Avi", 31) | A second object. __init__ runs again, with self = the second object. |
dana.grades.append(95) | Accessing Dana’s list and adding to it. |
avi.grades stays [] | Every __init__ created a new list, so there is no sharing. (Compare with the bug of def f(cart=[]) in the functions lesson!) |
Now the structure is guaranteed: you cannot create a Student without a name and an age, and every student always has grades.
Another example
A Product with default values, a calculated Attribute, and a first validation. week-3/product.py:
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative, got " + str(price))
self.name = name
self.price = price
self.quantity = quantity
self.in_stock = quantity > 0
laptop = Product("Laptop", 3500, 4)
cable = Product("USB Cable", 25)
print(laptop.name, laptop.price, laptop.quantity, laptop.in_stock)
print(cable.name, cable.price, cable.quantity, cable.in_stock)
try:
broken = Product("Ghost", -10)
except ValueError as error:
print("Could not create product:", error)Output:
Laptop 3500 4 True
USB Cable 25 0 False
Could not create product: price cannot be negative, got -10Breakdown:
quantity=0— a default value.Product("USB Cable", 25)gets a quantity of 0.self.in_stock = quantity > 0— an Attribute calculated from the data:TrueorFalse.- Validation in the Constructor:
__init__refuses to create a product with a negative price, and throws aValueError(rememberraise!). An invalid product cannot exist in the system. This is exactly “keeping the rules next to the data” that we talked about in the first chapter. - When
__init__throws an error, the object is not created, andbrokendoes not get a value.
Person — a third example, for reinforcement
class Person:
def __init__(self, first_name, last_name, city="Unknown"):
self.first_name = first_name
self.last_name = last_name
self.city = city
self.full_name = first_name + " " + last_name
noa = Person("Noa", "Levi", "Haifa")
tom = Person("Tom", "Cohen")
print(noa.full_name, "-", noa.city)
print(tom.full_name, "-", tom.city)Noa Levi - Haifa
Tom Cohen - UnknownSummary
__init__= the Constructor: called automatically when an object is created, and initializes its data.self= the specific object the method is working on right now. Always the first parameter, and Python passes it by itself.self.x = x:self.x= an Attribute saved on the object.x= a local parameter that disappears.obj = Class(a, b)→ Python creates an object, calls__init__(obj, a, b), and returns the object.- In
__init__you can have: default values, calculated Attributes, and validation withraise, so that an invalid object is never created at all.
Official documentation
Chapter 4: Methods — the Object’s Actions
What will we learn?
- What a Method is.
- How to define it and how to call it.
- Three kinds of methods by what they do: they calculate, they change state, they print.
- A method that calls another method.
__str__: how an object “presents itself”.
Why do we need this?
Until now our objects had only data. But the idea of OOP is data + actions together. In the first chapter we saw that the rules (like “you cannot withdraw more than the balance”) should sit next to the data. Methods are the place where these rules live.
And there is an open problem from the previous chapter: the car’s description was calculated once in __init__, and became outdated when the color changed. A method calculates anew every time.
Explanation from the basics
What is a Method?
Method = a function defined inside a Class, which acts on a specific object of that Class.
| Function (a regular function) | Method | |
|---|---|---|
| Where it is defined | At the top level of the file | Inside a Class |
| How you call it | function_name(args) | obj.method_name(args) |
| What it works on | On what you pass to it | On its object (self), and on what you pass to it |
| The first parameter | Anything you want | self |
You have already used methods: "hi".upper(), numbers.append(5). Today we write our own.
self in methods
Exactly as in __init__: when you call dana.add_grade(90), Python automatically passes dana as self. Inside the method, self.grades = Dana’s grades. If you call avi.add_grade(80), exactly the same method runs, but self is Avi.
dana.add_grade(90) → Student.add_grade(dana, 90)
↑ ↑
self gradeThe left line is what we write. The right line is what Python actually does.
Three kinds of methods (by what they do)
| Kind | What it does | Example |
|---|---|---|
| Getter | Calculates and returns a value from the data, without changing anything | average(), is_passing() |
| Setter | Changes the state of the object | add_grade(90), deposit(100) |
| Display | Prints / returns text for display | print_report(), __str__() |
A useful distinction from Clean Code: a good method is usually either a Getter or a Setter, not both together (SRP!).
State
State = the values of all the object’s Attributes at a certain moment. An account with a balance of 1,000 is in a certain state. After deposit(200), it is in a new state. Setter methods are the orderly way to change state.
__str__ — what the object looks like in print
Remember that print(dana) printed <__main__.Student object at 0x...>? If you define in the Class a special method named __str__ (Dunder again!) that returns a string, print and str() will use it to display the object.
Syntax
class ClassName:
def __init__(self, ...):
self.data = ...
def getter_method(self): # returns something, changes nothing
return ... self.data ...
def setter_method(self, value): # changes the object's state
self.data = ...
def uses_other_method(self):
return self.getter_method() * 2 # call another method on the same object
def __str__(self): # used by print() and str()
return "readable text about " + str(self.data)
obj = ClassName(...)
obj.setter_method(5) # no self in the call
result = obj.getter_method()
print(obj) # calls __str__ automatically- Every method is indented inside the Class, and
selfis first. - Between methods, put one empty line (PEP 8).
- Inside a method, you access the data and other methods with
self..
First code example
week-3/student_methods.py:
PASSING_GRADE = 60
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
if grade < 0 or grade > 100:
raise ValueError("grade must be between 0 and 100, got " + str(grade))
self.grades.append(grade)
def average(self):
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def is_passing(self):
return self.average() >= PASSING_GRADE
def __str__(self):
return self.name + " (average " + str(round(self.average(), 1)) + ")"
dana = Student("Dana")
dana.add_grade(90)
dana.add_grade(75)
avi = Student("Avi")
avi.add_grade(50)
print(dana)
print(avi)
print("Dana passing?", dana.is_passing())
print("Avi passing?", avi.is_passing())Output:
Dana (average 82.5)
Avi (average 50.0)
Dana passing? True
Avi passing? FalseBreaking down the code line by line
| Code | Kind | Explanation |
|---|---|---|
def add_grade(self, grade): | Setter | Changes the state: adds to self.grades. And first checks the rule (0–100). |
def average(self): | Getter | Calculates from the object’s data and returns. Changes nothing. Handles an empty list (the bug from the first chapter!). |
def is_passing(self): | Getter | Calls another method of the same object: self.average(). |
def __str__(self): | Display | Returns a string. print(dana) calls it automatically. |
dana.add_grade(90) | Python: Student.add_grade(dana, 90). self = dana. | |
print(avi) | Python: print(avi.__str__()), that is, "Avi (average 50.0)". |
The important point: the code that uses the Class (dana.add_grade(...)) does not need to know how the grades are stored (a list), and does not need to check the range itself. The Class is responsible for that.
Another example
A bank account — the problem from the first chapter, solved. week-3/bank_account.py:
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("initial balance cannot be negative")
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive, got " + str(amount))
self.balance = self.balance + amount
self.transactions.append("deposit " + str(amount))
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive, got " + str(amount))
if amount > self.balance:
raise ValueError("insufficient funds: balance " + str(self.balance) + ", requested " + str(amount))
self.balance = self.balance - amount
self.transactions.append("withdraw " + str(amount))
def transfer_to(self, other_account, amount):
self.withdraw(amount)
other_account.deposit(amount)
def statement(self):
print("--- Statement for", self.owner, "---")
for transaction in self.transactions:
print(" ", transaction)
print(" Balance:", self.balance)
def __str__(self):
return "Account of " + self.owner + ": " + str(self.balance) + " NIS"
dana = BankAccount("Dana", 1000)
avi = BankAccount("Avi")
dana.deposit(200)
dana.transfer_to(avi, 300)
try:
avi.withdraw(5000)
except ValueError as error:
print("Rejected:", error)
print(dana)
print(avi)
dana.statement()Output:
Rejected: insufficient funds: balance 300, requested 5000
Account of Dana: 900 NIS
Account of Avi: 300 NIS
--- Statement for Dana ---
deposit 200
withdraw 300
Balance: 900Breakdown:
transfer_to(self, other_account, amount)— a method that receives another object as a parameter. It does not change the balances directly. It usesself.withdrawandother_account.deposit, and that way all the checks run automatically. DRY!- If
withdrawfails (not enough money), it throws an error before thedeposit, so no “money is created out of thin air”. self.transactions— an internal history that every action updates. Whoever uses the account does not need to remember to record a transaction.statement()is a display method.__str__gives a short description forprint.- Compare with the procedural version from the first chapter: the same actions, but now there is one place that defines what an account is and what you are allowed to do with it.
Summary
- Method = a function inside a Class. Define it with
selffirst, and callobj.method(args)withoutself. obj.method(x)is equivalent toClass.method(obj, x). That is howself“knows” which object to work on.- Inside a method,
self.attributeandself.other_method(). - A Getter returns and does not change. A Setter changes the State (and checks the rules).
__str__returns a string, andprint(obj)uses it.- Derived information is better calculated in a method, and not stored as an Attribute that may become outdated.
Official documentation
Chapter 5: Shared Data — Instance Attribute Versus Class Attribute
What will we learn?
- Attribute versus Method: how to decide what each thing is.
- Instance Attribute: data of each object separately (what we have done until now).
- Class Attribute: data of the Class, shared by all the objects.
- When to use each one, and which traps there are.
Why do we need this?
Suppose there is a Class Product, and all the products are subject to the same VAT: 17%. Where do you keep the VAT?
- As an Instance Attribute (
self.vat = 0.17)? Then there are a thousand copies of the same number, and if the VAT changes, you have to update a thousand objects. - As a global constant at the top of the file? Possible, but it is “far” from the Class it belongs to.
And what if you want to know how many products were created in total? No single product “knows” that. It is information about the whole kind. Class Attributes solve both cases.
Explanation from the basics
Attribute or Method?
| Attribute | Method | |
|---|---|---|
| What it is | Data, what the object knows / holds | An action, what the object does / calculates |
| Part of speech | A noun / an adjective | A verb |
| Examples | name, price, balance, is_active | deposit(), add_grade(), total() |
| Access | obj.price | obj.total() with brackets |
Questions that help decide:
- Is it data that is stored, or something that is calculated? Stored data → an Attribute. Calculated from other data → usually a Method (we saw it: the
descriptionthat became outdated). - Does it change the object? Every orderly change → a Method (a command).
- Are rules needed when it changes? If so, the change goes through a Method (and we will go deeper into this in the lesson on Encapsulation).
Instance Attribute
Defined with self.x = ..., usually in __init__. Every object has its own copy, with its own value.
Class Attribute
Defined directly in the body of the Class, outside any method, without self:
class Product:
VAT_RATE = 0.17 # class attribute - one shared value
count = 0 # class attribute
def __init__(self, name):
self.name = name # instance attribute - per object- There is only one copy, which belongs to the Class itself.
- All the objects see it.
- You access it through the Class (
Product.VAT_RATE, the recommended way), or through an object (laptop.VAT_RATE, works for reading).
Instance Attributes = what every student keeps for themselves: a name, grades, a notebook. Class Attributes = what hangs on the classroom board and is shared by everyone: the homeroom teacher’s name, the room number, and how many students are in the class. When the teacher changes, you update the board once, and not in every student’s notebook.
How Python finds an Attribute
When you write obj.x, Python looks:
- First on the object (Instance Attributes).
- If not found, on the Class (Class Attributes and methods).
- Not found →
AttributeError.
(Notice: this is similar to LEGB from the Scope lesson, a lookup in layers.) This is also where the trap in the common mistake comes from.
Syntax
class ClassName:
SHARED_CONSTANT = 100 # class attribute (constant style: UPPER_CASE)
counter = 0 # class attribute (changes, shared)
def __init__(self, value):
self.value = value # instance attribute
ClassName.counter = ClassName.counter + 1 # update the class attribute through the CLASS
def uses_constant(self):
return self.value * ClassName.SHARED_CONSTANT
ClassName.SHARED_CONSTANT # read through the class
ClassName.counterFirst code example
week-3/products_shared.py:
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(laptop.name, laptop.price_with_vat())
print(mouse.name, mouse.price_with_vat())
print("Products created:", Product.created_count)
Product.VAT_RATE = 0.18
print("After VAT change:", laptop.price_with_vat(), mouse.price_with_vat())Output:
Laptop 1170.0
Mouse 58.5
Products created: 2
After VAT change: 1180.0 59.0Breaking down the code line by line
| Code | Kind | Explanation |
|---|---|---|
VAT_RATE = 0.17 | Class Attribute | One value for all the products. Capital letters = “a constant” (a convention). |
created_count = 0 | Class Attribute | A shared counter. It starts at 0 when the Class is defined. |
self.name, self.price | Instance Attributes | Different in every product. |
Product.created_count = Product.created_count + 1 | Every __init__ updates the Class’s counter, so it counts all the objects. | |
Product.VAT_RATE in the method | Reading a Class Attribute. | |
Product.VAT_RATE = 0.18 | One change, and all the products are affected immediately, because they all read from the same place. |
Another example
A class with a limit on places, and an automatic unique identifier. week-3/students_ids.py:
class Student:
SCHOOL_NAME = "Kodkod 6"
MAX_STUDENTS = 3
next_id = 1
all_students = []
def __init__(self, name):
if len(Student.all_students) >= Student.MAX_STUDENTS:
raise ValueError("class is full - cannot add " + name)
self.name = name
self.student_id = Student.next_id
Student.next_id = Student.next_id + 1
Student.all_students.append(self)
def __str__(self):
return "#" + str(self.student_id) + " " + self.name + " (" + Student.SCHOOL_NAME + ")"
for name in ["Dana", "Avi", "Noa", "Tom"]:
try:
Student(name)
except ValueError as error:
print("Error:", error)
for student in Student.all_students:
print(student)
print("Next id will be:", Student.next_id)Output:
Error: class is full - cannot add Tom
#1 Dana (Kodkod 6)
#2 Avi (Kodkod 6)
#3 Noa (Kodkod 6)
Next id will be: 4Breakdown:
next_id— a counter that produces a unique identifier for every student. Every object keeps its own number in the Instance Attributestudent_id, and the shared counter moves forward.all_students— a shared list in which the Class “registers” every object that was created.append(self)= “add this object to the list”.MAX_STUDENTS— a rule that applies to the whole Class. The check runs before the ID is assigned, so Tom did not “burn” a number.Student(name)in the loop without storing it in a variable: the object is not lost, because it is registered inall_students.
Class Attributes are excellent for constants (VAT_RATE, MAX_STUDENTS) and for counters. A shared list of “all the objects” is a good tool for demonstration, but in real projects it is usually better to manage the collection in a separate Class (for example, School that holds a list of students). We will learn this in the OOP design lessons (Composition).
Summary
- Attribute = what the object knows (a noun). Method = what it does (a verb, with brackets). Calculated information → a Method.
- Instance Attribute (
self.xin__init__) = every object has its own value. - Class Attribute (in the body of the Class, without
self) = one shared copy. Suits constants and counters. - Python looks for
obj.xfirst on the object, and then on the Class. - Change a Class Attribute only through the Class’s name.
obj.X = ...creates a new Instance Attribute. - A list/dict “per object” → in
__init__, not in the body of the Class.
Official documentation
Chapter 6: What Becomes a Class — From a Verbal Requirement to Classes
What will we learn?
Until now we told you which Classes to write. Now we move to the other side: you decide.
- How to find Classes inside a verbal description of a system.
- Four questions that decide: a Class, or not?
- When something is an Attribute, and when it is a Class of its own.
- Which Class each action belongs to.
Why do we need this?
The syllabus emphasizes: the goal is to understand OOP, and not only to know how to copy a Class. At work, nobody will tell you “write a Class named Order with these fields”. You will get a requirement: “we need a system in which customers order products…”, and you will have to turn it into a structure of Classes yourselves.
Good design at the start saves weeks of fixes. Bad design leads to one huge Class, or to dozens of empty Classes that do nothing.
Explanation from the basics
Tool 1: analyzing nouns and verbs
A simple and effective technique: read the requirement, and mark:
| In the text | A candidate for |
|---|---|
| Important nouns (“customer”, “order”, “product”) | Classes or Attributes |
| Adjectives and properties (“price”, “date”, “active”) | Attributes |
| Verbs (“orders”, “pays”, “cancels”) | Methods |
| Rules (“it is not possible to…”, “at most…”) | Validation / Invariants |
This is a starting point, not an answer. Not every noun becomes a Class.
Tool 2: four questions — is it a Class?
| # | Question | If the answer is “yes” |
|---|---|---|
| 1 | Does it have an identity and data of its own? (each one has different values) | Leans towards a Class |
| 2 | Does it have behavior or rules? (actions, validation) | Leans strongly towards a Class |
| 3 | Are there several instances of it? (many customers, many orders) | Leans towards a Class |
| 4 | Is it only a single value, without any behavior? (“color”, “name”) | An Attribute, not a Class |
“Car” — it has its own license plate (identity), it drives and brakes (behavior), and there are many cars. ✓ A Class. “The car’s color” — it is only a value (“red”), with no actions of its own. An Attribute of the car. “Engine” — it depends! In a simple game, an Attribute (engine_size = 1.6). In a garage that repairs engines, a Class (it has a serial number, a service history and a condition). The same concept, a different decision according to what the system needs.
Tool 3: a simple Attribute or a Class of its own?
A decisive question: do you need to keep more than one piece of information about this thing, or apply rules to it?
| Concept | A simple Attribute when… | A Class of its own when… |
|---|---|---|
| Address | One text is enough: "Herzl 10, Haifa" | You need the city, the street and the zip code separately, and to filter by city, and to validate the zip code |
| Price | One number | You need a currency, conversions and rounding by currency |
| Grade | A number | You also need a date, the name of the exam, a weight and an appeal |
Rule of thumb: start simple (an Attribute). When the value “grows” and gets more fields and rules, turn it into a Class.
Tool 4: which Class does the method belong to?
The golden rule: the method goes to the Class that holds the data it needs. (This is called “Information Expert”.)
- “Calculate the order total” → needs the order’s items →
Order.total(). - “Is the product in stock?” → needs the product’s quantity →
Product.is_in_stock(). - “How much did the customer spend this year?” → needs the customer’s orders →
Customer(if it holds its own orders), or a Class that manages all the orders.
A warning sign: a method in Class A that is mostly other.x, other.y, other.z → it probably belongs to the Class of other.
Syntax
There is no new syntax. The product of this stage is a design, which can be written as a code “skeleton”, with pass and docstrings:
class Order:
"""A customer's order: items, status and total."""
def __init__(self, customer):
self.customer = customer # has-a: the order holds its customer
self.items = []
self.status = "new"
def add_item(self, product, quantity):
pass
def total(self):
passA skeleton like this lets you think about the structure before writing logic.
First code example
The requirement: “A system for a coffee shop. Customers order drinks from the menu. Every drink has a name, a size (small/large) and a price. A large drink costs 3 NIS more. An order can include several drinks, and it has a total. A customer can cancel an order, as long as it has not been prepared.”
Step A — marking:
- Nouns: customer, drink, menu, order, name, size, price, total.
- Verbs: order, include, cancel, prepared.
- Rules: large = +3. Cancelling only before preparing.
Step B — the four questions:
| Candidate | Identity/data? | Behavior? | Several instances? | Decision |
|---|---|---|---|---|
| Drink | ✓ name, size, price | ✓ calculating the price by size | ✓ | Class Drink |
| Order | ✓ items, status | ✓ total, cancelling, preparing | ✓ | Class Order |
| Customer | ✓ name | A little | ✓ | Here: an Attribute (name) of Order. The requirement has no behavior for it. |
| Menu | A list of drinks | Maybe searching | One | Here: a simple dict. If rules are added, it will become a Class. |
| Size | A value (“small”/“large”) | — | — | An Attribute of Drink |
| Price | A number | — | — | An Attribute + a calculated method |
| Total | Derived | — | — | A Method Order.total() |
Step C — methods (“who holds the data?”):
price()→ Drink, because Drink knows the base price and the size.total()→ Order, because Order holds the drinks.cancel(),mark_prepared()→ Order, because Order holds the status.
Step D — code. week-3/coffee_shop.py:
LARGE_EXTRA = 3
class Drink:
def __init__(self, name, base_price, size="small"):
if size not in ("small", "large"):
raise ValueError("size must be small or large, got " + size)
self.name = name
self.base_price = base_price
self.size = size
def price(self):
if self.size == "large":
return self.base_price + LARGE_EXTRA
return self.base_price
class Order:
def __init__(self, customer_name):
self.customer_name = customer_name
self.drinks = []
self.status = "open"
def add(self, drink):
if self.status != "open":
raise ValueError("cannot add drinks to a " + self.status + " order")
self.drinks.append(drink)
def total(self):
return sum(drink.price() for drink in self.drinks)
def mark_prepared(self):
self.status = "prepared"
def cancel(self):
if self.status == "prepared":
raise ValueError("cannot cancel - the order was already prepared")
self.status = "cancelled"
MENU = {"espresso": 9, "latte": 14, "tea": 8}
order = Order("Dana")
order.add(Drink("latte", MENU["latte"], "large"))
order.add(Drink("espresso", MENU["espresso"]))
print(order.customer_name, "pays", order.total())
order.mark_prepared()
try:
order.cancel()
except ValueError as error:
print("Rejected:", error)Output:
Dana pays 26
Rejected: cannot cancel - the order was already preparedBreaking down the code line by line
| Design decision | What it looks like in the code |
|---|---|
| Drink = a Class | class Drink with data (name, price, size) and behavior (price). |
| Size = an Attribute with a rule | self.size, with validation in __init__. |
| “Large = +3” belongs to the drink | Drink.price(), because the drink knows its own size. |
| Order = a Class | class Order with items and a status. |
| Customer = an Attribute (for now) | self.customer_name, a string. |
| “Cancelling only before preparing” | Validation in Order.cancel(). |
| Menu = a dict | MENU. It has no behavior, so there is no need for a Class. |
| Total = a method | total() calculates from drinks, and is not stored as data that may become outdated. |
Another example
When the requirement changes — the customer becomes a Class. A requirement was added: “Customers have a club card: every purchase earns a point for every 10 NIS, and at 50 points you get a free drink.”
Now the customer has data (points), behavior (earning, redeeming) and rules (50 points). According to the four questions, it is already a Class:
POINTS_PER_SHEKEL = 0.1
FREE_DRINK_POINTS = 50
class Customer:
def __init__(self, name):
self.name = name
self.points = 0
def earn_points(self, amount_paid):
self.points = self.points + int(amount_paid * POINTS_PER_SHEKEL)
def has_free_drink(self):
return self.points >= FREE_DRINK_POINTS
def redeem_free_drink(self):
if not self.has_free_drink():
raise ValueError(self.name + " has only " + str(self.points) + " points")
self.points = self.points - FREE_DRINK_POINTS
dana = Customer("Dana")
for payment in [120, 260, 140]:
dana.earn_points(payment)
print(dana.name, "has", dana.points, "points. Free drink?", dana.has_free_drink())
dana.redeem_free_drink()
print("After redeeming:", dana.points)Output:
Dana has 52 points. Free drink? True
After redeeming: 2And in the structure: Order now holds self.customer (a Customer object) instead of customer_name, and when an order is paid, it calls self.customer.earn_points(self.total()). The same concept, “customer”, started as an Attribute and became a Class when the requirements grew. That is natural, and it is the right work process.
Summary
- Nouns → candidates for Classes/Attributes. Verbs → Methods. Rules → Validation.
- A Class when there is identity + data, behavior or rules, and several instances. A single value without behavior → an Attribute.
- The same concept can be an Attribute in one system and a Class in another. Start simple, and upgrade when the requirements grow.
- A Method belongs to the Class that holds the data it needs (Information Expert).
- Sometimes an “action” (a loan, an order) is really a Class, when it has data and logic of its own.
- Not a Class for every noun, and not a God Class that does everything.
Official documentation
Chapter 7: Back to the Bank — Everything Together
What will we learn?
We return to the bank system from the start of the lesson, and rebuild it — this time with everything we learned in the previous chapters.
Why do we need this?
Every chapter added one tool and solved one problem. Here you see how all of them work together in one Class — and that is exactly what you will do on your own in the mini project.
The bank — with everything we learned
class BankAccount:
accounts_opened = 0 # shared by all accounts
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("balance cannot be negative")
self.owner = owner # per account
self.balance = balance
BankAccount.accounts_opened = BankAccount.accounts_opened + 1
def deposit(self, amount): # setter - changes the state
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def __str__(self):
return self.owner + ": " + str(self.balance)
dana = BankAccount("Dana", 1000)
dana.deposit(200)
print(dana) # Dana: 1200| Part of the code | From which chapter |
|---|---|
class BankAccount — one template for all the accounts | Chapter 2 |
__init__ with a check — every account is born complete and valid | Chapter 3 |
deposit — a Setter that protects the rules | Chapter 4 |
__str__ — what the account looks like in print | Chapter 4 |
accounts_opened — a counter shared by all the accounts | Chapter 5 |
The four problems from the first chapter were solved: you cannot create an account with a negative balance, the data and the actions sit together, the structure has one definition, and the Class itself enforces the rules.
Summary
- A Class is a template, and an Object is an instance built from it — with data of its own.
__init__runs at creation and guarantees that the object is born complete;selfis the current object.- Methods are the actions: a Getter returns information, a Setter changes the state and checks the rules, and
__str__decides whatprintshows. - A Class Attribute is one piece of data shared by all the objects — like a counter.
Official documentation
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
What you practice on this page: why OOP is needed, creating a Class with __init__ and checking values, methods (Getter and Setter) and __str__, data shared by the whole Class, and design — what is a Class, what is an Attribute and what is a Method.
By the end of the page you will know how to:
- Recognize when it is better to combine data and actions into one object
- Write a complete Class:
__init__, methods and__str__ - Use a Class Attribute as a counter shared by all the objects
- Turn a verbal description into a list of Classes, Attributes and Methods
Exercise 1: Spot the problems
What you do: read existing code and find problems in it — without writing code.
The code:
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def average(student):
return sum(student["grades"]) / len(student["grades"])The task: find three problems that come from the data (the dictionaries) and the actions (the function) being separate. For each problem write one sentence: what the problem is, and on which line it appears.
What to submit: three problems, one sentence for each.
Show solution
- A typo creates a new key —
student3has"nmae"instead of"name". There is no defined structure that would catch this, and the problem will be discovered only when someone triesstudent3["name"]. - There are no rules — any code can put
-50or"abc"intogrades, and nobody checks. - The data and the actions are scattered —
averageis a separate function that receives a dictionary, and does not know whether it is a student at all. And with an empty list (student3) it will fail with aZeroDivisionError.
In OOP, Student defines once which data it has and which actions it knows how to do — all together.
Exercise 2: A check at creation
What you build: a product in a shop — that cannot be created with a negative price.
What to write:
- A Class named
Product. - An
__init__that receives a name, a price and a quantity. If the quantity was not passed — it is 0. - If the price is negative: throw a
ValueErrorwith the message that appears in the output, and save nothing. - Otherwise: save the three pieces of data on the object, with
self.
Test code:
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)The desired output:
Pen 5 0
error: price cannot be negativeShow solution
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative")
self.name = name
self.price = price
self.quantity = quantity
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)Pen 5 0
error: price cannot be negativeThe check in __init__ guarantees that there is never a Product object with an invalid price.
Exercise 3: Student with methods and str
What you build: a student who manages their own grades.
What to write:
- A Class named
Student. The__init__receives a name and creates an empty grade list. add_grade— receives a grade and adds it to the list. This is a Setter: it changes the state.average— returns the average, or 0 if there are no grades yet. This is a Getter: it only returns.__str__— returns the name and the average, exactly as in the last line of the output.
Test code:
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)The desired output:
0
82.5
Dana (average 82.5)Show solution
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def average(self):
if len(self.grades) == 0:
return 0
return sum(self.grades) / len(self.grades)
def __str__(self):
return self.name + " (average " + str(self.average()) + ")"
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)0
82.5
Dana (average 82.5)add_grade is a Setter — it changes the state. average is a Getter — it returns information without changing anything. print calls __str__ automatically.
Exercise 4: A shared counter
What you build: a product that knows its price including VAT — and a Class that counts how many products were created.
What to write:
- A Class named
Product, with an__init__that receives a name and a price. - Two Class Attributes — data that belongs to the whole Class, not to one object:
VAT_RATEwith the value 0.17, andcreated_countthat starts at 0. - Every time a product is created, increase
created_countby 1. price_with_vat— returns the price including VAT.
Test code:
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())The desired output:
2
1170.0Show solution
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())2
1170.0nameandpricebelong to each product separately — Instance Attributes.VAT_RATEandcreated_countare shared by all the products — Class Attributes.- The counter is updated through
Product.created_count— not throughself.
Exercise 5: Nouns and verbs
What you do: design only, without code — turn a verbal description into a list of Classes, Attributes and Methods.
The requirement:
“In a coffee shop, customers order drinks in different sizes. Every drink has a base price, and a large drink costs 3 NIS more. The customer earns points on every order, and can redeem them.”
The task: build a table with three columns: the candidate, what it is (a Class, an Attribute or a Method), and why.
Hint: nouns are candidates for Classes or Attributes. Verbs are candidates for Methods.
What to submit: a table with all the candidates from the requirement.
Show solution
| Candidate | What it is | Why |
|---|---|---|
Customer (Customer) | Class | A noun with data (name, points) and behavior (earns, redeems) |
Drink (Drink) | Class | A noun with data (base price, size) and a rule (large costs more) |
| Base price, size | Attributes of Drink | Simple values that describe a drink |
| Points | Attribute of Customer | A simple number |
| Earns points, redeems | Methods of Customer | Verbs — actions on the customer’s data |
| Final price | Method of Drink | Calculated from the price and the size |
Mini project: A small library
Which system you build: a small system for a library. The library has books, and every book has several copies. Readers borrow copies and return them, and at every moment the system knows how many copies of each book are free — and how many books there are in the library in total.
What you will learn from the project:
- Build a complete Class from scratch:
__init__, methods and__str__ - Check values already at creation, and throw an error when they are invalid
- Tell apart a Setter that changes the state and a Getter that only returns information
- Use a Class Attribute as a counter shared by all the objects
What to build — step by step:
Step 1 — the Class and the shared counter. Write a Class named Book, with a Class Attribute named total_books that starts at 0. It will count how many books were created.
Step 2 — creating a book. The __init__ receives a title, an author and a number of copies, and saves them. At the start all the copies are free — save them also in available. Every new book increases total_books by 1. If the number of copies is smaller than 1 — throw a ValueError, and do not create a book.
Step 3 — borrowing and returning (Setters).
borrow— takes away one free copy. If there are no free copies:ValueError.return_book— returns one copy. If all the copies are already in the library:ValueError.
Step 4 — checking (Getter). is_available returns True if there is at least one free copy, otherwise False.
Step 5 — display. __str__ returns the title, the author and how many copies are free out of how many, as in the output.
The error messages — exactly like this:
copies must be at least 1
no copies available
all copies are already inTest code:
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)The desired output:
Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2By the end of the project you will know how to:
- Translate a description of a “thing” in the world — a book — into a Class with data and actions
- Keep the rules inside the object, so that it cannot reach an invalid state
- Understand why the book Empty is not counted by the counter: the error was thrown before the increase
Bonus: add a Class named Library that holds a list of books, with add_book that adds a book, and available_books that returns only the books that can be borrowed right now.
Show solution
class Book:
total_books = 0
def __init__(self, title, author, copies):
if copies < 1:
raise ValueError("copies must be at least 1")
self.title = title
self.author = author
self.copies = copies
self.available = copies
Book.total_books = Book.total_books + 1
def borrow(self):
if self.available == 0:
raise ValueError("no copies available")
self.available = self.available - 1
def return_book(self):
if self.available == self.copies:
raise ValueError("all copies are already in")
self.available = self.available + 1
def is_available(self):
return self.available > 0
def __str__(self):
return (self.title + " by " + self.author + " ("
+ str(self.available) + "/" + str(self.copies) + " available)")
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2total_booksis a Class Attribute — one counter for all the books. The bookEmptywas not created (the error was thrown before the increase), so the counter is 2.borrowandreturn_bookare Setters — they change the state, and check the rules before the change.is_availableis a Getter — it returns information without changing anything.
Bonus:
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def available_books(self):
result = []
for book in self.books:
if book.is_available():
result.append(book)
return result
library = Library()
for book in books:
library.add_book(book)
for book in library.available_books():
print(book)Clean Code by Martin (2/3 available)