- Learn
- Computer Science & IT
- Programming
- Introduction to Programming with Python
- Python Classes and OOP
Python Classes: Object-Oriented Programming (OOP) in Python
==, sorting and + through special methods, build programs in which objects hold and call other objects, and judge when a class makes a program clearer than functions and dictionaries.BeginnerLesson 10 of 10 in Python for Beginners: Introduction to Programming
What you will learn
- See that every Python value is an object with its own data and methods, then write a class of your own and create objects from it.
- Recognize the objects you already useDescribe strings, lists and dictionaries as objects that have a type, hold data and offer methods you call with dot notation.
- Define a class and create instancesWrite a class statement, create several separate objects from it, and set and read their attributes with dot notation.
- Recognize the objects you already use
- Use a constructor to give every new object the attributes it needs, choose between instance and class attributes, and stop invalid objects from being created.
- Write a constructor with __init__ and selfDefine
__init__so that each new object starts with its own attributes, taken from the arguments or set to starting values. - Choose instance attributes or class attributesDecide whether a value belongs to each object or to the class as a whole, and predict how Python finds and changes each kind.
- Keep objects valid from the startCheck the arguments a constructor receives, raise a clear exception for bad values, and keep the rules that make an object meaningful.
- Write a constructor with __init__ and self
- Write methods that use and change an object's data, and protect that data from changes that would break the object.
- Define and call instance methodsWrite methods that work with
self, call them on objects, and return results instead of printing them. - Change an object's state with methodsWrite methods that update attributes while keeping the object valid, and predict when a change is visible through other names for the same object.
- Protect attributes with naming conventions and propertiesMark attributes as internal with an underscore, and use properties to check new values or to compute values that stay up to date.
- Define and call instance methods
- Write special methods that let your objects show readable text, compare with
==and<, and work with operators and built-in functions.- Give objects readable string representationsWrite
__str__and__repr__so that objects show useful text when printed, inspected or stored in a list. - Compare and combine objects with operatorsDefine
__eq__,__lt__,__add__and__len__so that your objects work with==, sorting,+andlen(), and decide when an operator is clearer than a named method.
- Give objects readable string representations
- Combine objects so that each one handles its own data, trace programs where objects call each other, and decide when a class makes a program clearer.
- Build objects that contain other objectsStore objects as attributes or in lists inside another object, and write methods that work across the whole collection.
- Make objects interact through methodsPass objects to methods and functions, return new objects, and trace a short program in which several objects work together.
- Decide when a class makes a program clearerJudge when bundling data and behavior in a class helps, plan a small class from a description, and rewrite dictionary-based code as a class.
- Build objects that contain other objects
Python Classes and OOP study guide
1Understand objects and define your first class
Objects already appear in every Python program you write: a string has methods, a list changes its contents, and a dictionary answers lookups. Reading those familiar operations as object behavior gives you a starting point for classes of your own. Learn how types, identity, and shared references affect a program, then define a small class and give separate instances their own attributes.
1.1 Recognize the objects you already use
You have already used objects whenever you have worked with a number, string, list, or dictionary. An object has a type, a value or state, and operations it supports. Even functions and None are objects. Writing your own class extends this familiar system.
Find an object's type and methods
type("hello") returns <class 'str'>; type([1, 2]) returns <class 'list'>. The words type and class describe the same role here. isinstance(3.5, float) is True, while isinstance("3.5", float) is False: the second value is text, even though it looks numeric.
A method is a function provided by a type. In "ada lovelace".title(), the string before the dot is the object, title is the method, and the parentheses call it. The result is "Ada Lovelace". dir(value) lists names offered by an object; names such as __len__ are special methods that connect objects to Python operations such as len(value).
Track the result and the original
A method call can return a result, change an object, or do both. Check the method's behavior before deciding what to assign.
word = "level"
print(word.count("l")) # 2
print(word.upper()) # LEVEL
print(word) # level
nums = [3, 1, 2]
result = nums.sort()
print(nums) # [1, 2, 3]
print(result) # None
Strings are immutable: their contents cannot change. upper() returns another string, so word keeps its original value. To keep the uppercase result under that name, write word = word.upper().
Lists are mutable. sort() changes the list in place and returns None. Writing nums = nums.sort() loses the list reference held by nums and assigns None. Use nums.sort() on its own line, or nums = sorted(nums) to get a new sorted list. For a dictionary, {"a": 1}.get("b", 0) returns 0 because the requested key is missing.
Parentheses matter too. word.upper is a method object; word.upper() runs it. The same behavior can be offered by many objects: "cat".upper() and "dog".upper() use one kind of method with different string data.
Separate equality from identity
Equality asks whether values match. Identity asks whether two names refer to one object.
a = [1, 2]
b = [1, 2]
c = a
print(a == b) # True
print(a is b) # False
print(c is a) # True
c.append(3)
print(a) # [1, 2, 3]
print(b) # [1, 2]
a and b start with equal contents but are separate lists. c = a creates an alias, another name for the same list. Appending through c changes the object that a also refers to. It does not change the separate list held by b. When tracing a program, draw one box per object and an arrow from each name to its box.
Key Takeaways
- Every Python value is an object with a type, state, and supported operations.
- Call a method with parentheses; a method reference alone does not run it.
- Check whether a method returns a new value or changes the existing object.
==compares values;iscompares identity.- Assignment can give two names references to the same mutable object.
1.2 Define a class and create instances
A class defines a type you can use in your own program. Each object made from it is an instance. One class describes the kind of thing; instances hold the data for particular things. A blank form and its filled-in copies make a useful comparison.
Write a class and call it
A class statement has the keyword class, a name, a colon, and an indented body. pass supplies an empty body while you try out the structure.
class Book:
pass
b1 = Book()
b2 = Book()
print(b1 is b2) # False
print(isinstance(b1, Book)) # True
Each Book() call makes a separate instance. In a script, type(b1) shows <class '__main__.Book'>. The class statement must run before code tries to use Book. You can also define a class in a module and import it with from library import Book, then create instances in the calling program.
Follow Python's naming conventions: Book, BankAccount, and ShoppingCart use CapWords, with each word capitalized. Variables and methods use lowercase words separated by underscores, such as book_count and add_item. Use four spaces for each level of indentation.
Store data on each instance
An attribute is a named value reached with a dot. Assigning an attribute stores it; reading the same name retrieves it.
b1.title = "Dune"
b2.title = "Emma"
b1.pages = 412
print(b1.title) # Dune
print(b2.title) # Emma
print(b1.pages) # 412
The data belongs to the selected instance. Giving b1 a pages attribute does not give one to b2. Reading b2.pages now raises AttributeError, because that object has no attribute named pages.
Assignments can create new names without checking what you meant. After b1.titel = "X", the original b1.title still holds "Dune"; the object has gained a second attribute with a misspelled name. If a value seems not to change, compare the spelling where it was assigned and where it was read.
Keep the class and its instances distinct
| Expression | Meaning |
|---|---|
Book | The class object, defining the type |
Book() | A call that creates an instance |
b1.title | The title stored on this book |
Book.title | An attribute requested on the class itself |
b = Book gives the class another name. It does not create a book. If titles were set only on instances, asking for Book.title raises AttributeError; there is no single title shared by the type.
Give every object a dependable start
Setting attributes from outside is possible, but it leaves setup scattered across the program. One book might receive a title and page count while another receives only a title. Later code cannot safely assume that both have pages.
A setup method called __init__ places the starting assignments inside the class. Each creation call then follows the same setup process. The class can require a title and create other attributes with sensible initial values, so calling code does not have to remember a separate sequence of assignments.
Key Takeaways
- A class defines a type; an instance is one object made from it.
- Use
ClassName()to create an instance and dot notation to reach its attributes. - Each instance can hold different data under the same attribute names.
- A missing attribute raises
AttributeError; a misspelled assignment can create a new attribute.
2Set up objects with constructors and attributes
An object needs a dependable starting state before its methods can do useful work. Initialization connects the arguments in a class call to attributes stored on the new instance. Choose which values belong to one object and which belong to the class, then validate inputs before accepting them. Tracing lookup and setup failures will help you spot accidental sharing and missing data.
2.1 Write a constructor with __init__ and self
Every newly created account needs an owner and a starting balance. Putting that setup in __init__ gives each object the attributes that later methods expect. Python creates the instance first, then calls __init__ to initialize it.
Connect arguments to attributes
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
acct = BankAccount("Maya", 50)
other = BankAccount("Leo")
For the first call, Python passes the new account as self, "Maya" as owner, and 50 as balance. The assignments make acct.owner equal "Maya" and acct.balance equal 50. The second call uses the default balance, so other.balance is 0.
self is the parameter that receives the current object. It is a convention, not a Python keyword. Use this name so readers can recognize the object immediately. Callers leave it out: BankAccount("Maya", 50) supplies two arguments, not three.
Keep local names and stored data separate
In self.owner = owner, the name on the right is a local parameter. The name on the left is an attribute stored on the object. When initialization ends, other methods can still use self.owner.
Writing balance = balance only reassigns the local name. It creates no attribute, so reading acct.balance later fails. Put self. before each attribute that should remain part of the object.
Plan initialization by listing what each instance needs. Required values become parameters after self; optional values can have defaults. Values that always start the same way can be assigned in the body, such as self.visits = 0. A list created by self.transactions = [] is made anew on every call.
Avoid a shared default list
A default argument is evaluated when Python defines the method. With def __init__(self, items=[]), calls that omit items receive the same list. If the initializer stores that list directly, changing one object's items changes the items seen by another.
Use None as the default and create a fresh list during each call:
class Cart:
def __init__(self, items=None):
self.items = [] if items is None else list(items)
a = Cart()
b = Cart()
a.items.append("pen")
print(b.items) # []
list(items) also makes a new outer list when a caller supplies one. The objects inside that list are still shared references; creating the list does not recreate every item.
Read setup errors
BankAccount() is missing the required owner argument and raises TypeError. Check the call against the parameters after self.
The spelling __init__ has two underscores on each side. _init_ is an ordinary method name and will not perform this setup automatically. __init__ must return None, so normally leave out a return statement. Returning a balance raises TypeError; the class call itself supplies the new instance to the caller.
Key Takeaways
__init__initializes the instance after Python creates it.- Python supplies
self; callers supply the remaining arguments. self.name = namestores a parameter as an instance attribute.- Create per-instance lists inside initialization; use
Noneinstead of a mutable default. __init__must returnNone.
2.2 Choose instance attributes or class attributes
Some values describe one object; others describe the type as a whole. A pizza's size belongs to that pizza. A topping limit or a count of pizzas created can belong to the class. Where an assignment occurs determines which kind of attribute it creates.
Put the value in the right place
class Pizza:
MAX_TOPPINGS = 5
count = 0
def __init__(self, size):
self.size = size
self.toppings = []
Pizza.count += 1
p1 = Pizza("large")
p2 = Pizza("small")
size and toppings are instance attributes, set on self. Each pizza has its own values. MAX_TOPPINGS and count are class attributes, assigned in the class body outside a method. Capital letters mark a value intended as a constant; they do not prevent reassignment.
After these calls, Pizza.count is 2. Every initializer updates the same class attribute. p1.size is "large", while p2.size is "small".
Trace lookup and shadowing
For ordinary data attributes such as these, reading p1.MAX_TOPPINGS first checks the instance and then the class. Since no value is stored on p1, Python finds Pizza.MAX_TOPPINGS, which is 5.
Assignment through an instance has a different effect:
p1.MAX_TOPPINGS = 8
Pizza.MAX_TOPPINGS = 6
print(p1.MAX_TOPPINGS) # 8
print(p2.MAX_TOPPINGS) # 6
print(Pizza.MAX_TOPPINGS) # 6
The first line creates an instance attribute on p1. It shadows the class value for that object. Changing the class value later reaches p2, which still reads from the class, but does not replace p1's own value.
| Assignment | What changes |
|---|---|
self.size = size | Data on the current instance |
Pizza.count += 1 | The shared class counter |
p1.MAX_TOPPINGS = 8 | An instance value hiding the class value |
Watch two forms of accidental sharing
If the initializer used self.count += 1, Python would read the class's 0, add one, and store an instance attribute. Each new pizza would receive its own count of 1, while Pizza.count stayed 0. Name the class when updating a class-wide counter.
A list in the class body causes a different problem. With toppings = [], instances that have no separate toppings attribute all find the same list. p1.toppings.append("olives") mutates that shared list; it does not assign a new instance attribute. Put self.toppings = [] in __init__ when each pizza needs its own toppings.
Inspect an object's own data
For the original p2, vars(p2) gives {'size': 'small', 'toppings': []}. The class values are absent because they live on Pizza. This is useful when debugging a surprising lookup: check whether the instance has its own value, then inspect the class value. A shared setting belongs on the class; a name, size, balance, or personal collection belongs on the instance.
Key Takeaways
- Instance attributes hold data for one object; class attributes hold shared settings or class-wide data.
- An instance data attribute can shadow a class attribute of the same name.
- Update a shared counter through the class name.
- Mutating a class-level list changes the one shared list.
vars(obj)shows its own stored attributes, not the class attributes it can find.
2.3 Keep objects valid from the start
An object should start in a state its methods can use. A rectangle with a negative width cannot represent the intended shape. A rule that must remain true for every object of a class is an invariant. Validation checks those rules before accepting data.
Check before storing
class Rectangle:
def __init__(self, width, height):
if width <= 0:
raise ValueError("width must be positive")
if height <= 0:
raise ValueError("height must be positive")
self.width = width
self.height = height
Rectangle(3, 4) passes both checks and returns an initialized object. Rectangle(-2, 4) raises ValueError at the first check. The later assignments never run. If the program does not catch that exception, execution stops there.
Suppose r already refers to a valid rectangle. In r = Rectangle(-2, 4), the right-hand call fails before assignment. r keeps its previous value. If r had no value before the failed call, the assignment does not create one.
Choose a useful exception
| Exception | Problem it describes |
|---|---|
ValueError | An acceptable kind of input has an unacceptable value |
TypeError | The input has the wrong type for the operation |
A negative numeric width is a value error. Text supplied where a number is required is a type error. The comparison with 0 in this example already raises TypeError for text. A class can also check a type explicitly with isinstance when it needs a clearer message.
Write messages that name the rule, such as "width must be positive". A message saying only "bad input" leaves the caller guessing which value needs repair.
Let calling code handle recovery
while True:
try:
width = float(input("Width: "))
room = Rectangle(width, 3)
break
except ValueError as error:
print("Try again:", error)
Entering abc fails during float; entering -2 passes conversion but fails in Rectangle. Both raise ValueError, so this loop handles both. The class states its data rule, while the calling code decides to ask again.
Printing a warning inside __init__ does not reject the call. If execution continues, the program can receive an invalid object and fail later when using it. Raising an exception stops the unsuccessful setup where the problem occurs.
Clean data deliberately and maintain the rule
A documented cleanup can be useful. self.name = name.strip() removes surrounding spaces; validate the cleaned name if blank names are forbidden. self.items = list(items) makes a separate outer list, so appending to the caller's list does not append to this one. Changing an invalid width to its absolute value would change the caller's meaning, so it needs an explicit design rule.
Initialization checks run at creation time. Plain assignment such as r.width = -5 still bypasses them later. Methods that change a width must check it again. A property can place a check on attribute assignment itself, allowing the same rule to run during setup and later updates.
Key Takeaways
- An invariant is a rule that valid object state must satisfy.
- Validate before accepting data and raise a clear exception for a failure.
- A failed creation call does not assign a new object to the caller’s name.
- Constructor checks also need support in later methods or properties that change state.
3Give objects behavior with instance methods
Methods turn stored data into useful behavior. A rectangle can calculate its area, and an account can update its balance while recording a transaction. Follow how Python passes the receiving object into each method, then distinguish a returned result from a change in state. Properties let a class keep readable attribute access while checking assignments or computing values on demand.
3.1 Define and call instance methods
An instance method is a function defined inside a class that works with the object passed as self. The object supplies its stored data; extra parameters supply any additional information needed for the action.
Read attributes through self
Add these methods inside the validated Rectangle class, at the same indentation level as __init__:
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def is_square(self):
return self.width == self.height
With r = Rectangle(3, 4), r.area() returns 12, r.perimeter() returns 14, and r.is_square() returns False. Each method reads the same object's attributes. A second rectangle uses the same method definitions with its own width and height.
Inside a method, self.width names an attribute. A bare width names a local or other accessible variable; Python does not automatically search the current object for it. If no such variable exists, return width * height raises NameError. Writing self. makes the intended data owner explicit.
Follow the call
For this class, r.area() works like Rectangle.area(r). Python finds the function on the class and supplies r as its first argument. A method reference such as r.area is a bound method: it remembers the function and this rectangle. Adding () performs the call.
If a method is defined as def resize(self, factor), call it as r.resize(2). r fills self, and 2 fills factor. You do not pass r again. If an instance method is accidentally written as def area():, a call on r raises TypeError because Python still supplies the object to a function that accepts no arguments.
One method can use another through self:
def double_area(self):
return self.area() * 2
Here self.area() calculates using the same rectangle. Leaving out self. would ask for a separate function called area, not the object's method.
Return results for the caller
A calculation method should normally return its result. Returning gives calling code a value to store, combine, test, or display:
r1 = Rectangle(3, 4)
r2 = Rectangle(2, 5)
total = r1.area() + r2.area()
print(total) # 22
If area only printed its answer, each call would return None. The addition would then fail with TypeError, even though two numbers had appeared on the screen. Printing is an action; returning supplies a value to the expression containing the call.
Use method names that show the task. deposit and resize name actions; is_square and has_room suggest Boolean checks. A method does not need parameters for data already stored on its object. It needs parameters for new inputs, such as a resize factor or a deposit amount.
Key Takeaways
- Put
selffirst in an instance method; Python supplies the receiving object. - Read attributes and call other methods through
self.. - Parentheses run a bound method.
- Return calculated values so callers can combine, test, or display them.
3.2 Change an object's state with methods
An object's state is the current set of values in its attributes. An accessor reports on that state. A mutator changes it. A useful mutator checks its inputs and maintains every related attribute, so calling code does not have to coordinate several updates.
Validate the whole change first
Add these methods inside the BankAccount class with its owner, balance, and transaction list:
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self.balance += amount
self.transactions.append(("deposit", amount))
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive")
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
self.transactions.append(("withdraw", amount))
A deposit changes the balance and records the event. A withdrawal checks both the amount's sign and the available balance before subtracting. These checks prevent a rejected withdrawal from changing either the balance or its history.
Starting with BankAccount("Maya", 50), depositing 25 gives 75, and withdrawing 30 gives 45. A later attempt to withdraw 100 raises ValueError; it leaves the balance at 45 and adds no transaction. Putting the subtraction before the funds check would change state even on failure.
Distinguish a change from a returned value
These mutators have no return statement, so they return None. After new_balance = acct.deposit(25), the deposit has happened, but new_balance is None. Read acct.balance afterwards to obtain the updated number.
A class can offer a different style of method that returns a new object. resize suggests changing a rectangle; resized can be chosen for a method that returns a new rectangle and leaves the original alone. Read the method's contract: the name is helpful, but its definition determines the behavior.
Trace shared references
a = BankAccount("Maya", 50)
b = a
b.deposit(10)
print(a.balance) # 60
print(a is b) # True
Assignment binds a name to an object; it does not copy that object. Both names reach the account whose balance changed. Passing a to a function also passes access to that object. A function calling account.withdraw(5) can therefore change the account seen by its caller.
For an independent account with the same owner and balance, create BankAccount(a.owner, a.balance). That constructor also supplies a fresh, empty history. It is a new account, not a complete historical copy.
copy.copy(a) creates a new outer object but keeps references to the same inner objects, including the transaction list. copy.deepcopy(a) copies nested objects too. When independence matters, check the objects inside the attributes as well as the outer instance.
Directly assigning acct.balance = -500 skips the checks in the two methods. Internal attribute conventions and properties help make the intended update path clear and keep validation close to state changes.
Key Takeaways
- Check a requested change before updating attributes.
- A mutator can change state while returning
None. - Assignment and argument passing preserve references to the same object.
- A shallow copy can still share nested mutable objects.
- Keep related state updates together in the method responsible for them.
3.3 Protect attributes with naming conventions and properties
Calling code needs a clear way to read and change an object. Encapsulation keeps its data rules with the methods that manage them. Python uses conventions and properties to express that interface.
Mark internal storage
A leading underscore in _balance signals an internal attribute. Callers should use the account's methods, but Python still permits direct access. There is no private keyword that locks the value away.
A name such as __balance, with two leading underscores and no double trailing underscores, triggers name mangling. Inside class Acc, self.__balance is stored under _Acc__balance. Outside, obj.__balance fails, but obj._Acc__balance can still work. Mangling prevents name clashes; it does not secure sensitive data.
Run a method through attribute syntax
A property makes a method run when an attribute is read. The @property line is a decorator: it changes how Python exposes the method. A property without a setter permits reading but rejects assignment.
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balance
After acct = Account("Maya", 50), read acct.balance with no parentheses. Assigning acct.balance = 10 raises AttributeError because this property has no setter. A deposit method inside the class could update _balance after its checks.
Check assignment with a setter
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not 0 <= value <= 100:
raise ValueError("score must be between 0 and 100")
self._score = value
The getter and setter share the name score; the stored value uses _score. On creation, self.score = score runs the setter.
For s = Student("Ana", 88), assigning s.score = 92 stores 92. Assigning s.score = -1 raises ValueError before storage, so the score stays 92. Student("Ana", 105) fails during initialization.
Inside the setter, self.score = value would call the setter again, repeating until RecursionError. The getter has the same risk if it returns self.score. Use the separate storage name _score in both bodies.
Compute values when they are read
A property can calculate a value from other attributes:
@property
def area(self):
return self.width * self.height
Placed inside a rectangle class, this permits r.area. For width 3 and height 4, the value is 12. Changing the width to 5 makes the next read return 20. Storing an area once during initialization would leave an old value behind after a width change.
Start with a plain attribute when simple storage meets the need.
Key Takeaways
- A leading underscore marks internal storage by convention; it does not block access.
- Name mangling changes an attribute name to avoid clashes.
- A property runs a getter through attribute syntax.
- A setter can validate initialization and later assignments in one place.
- Use a separate backing attribute to avoid calling the property recursively.
- A computed property derives its value when read.
4Use special methods so objects print and compare well
A useful object should fit naturally into ordinary Python expressions. Its special methods can supply readable text, value equality, an ordering, arithmetic results, or a length. Learn which method Python requests in each setting and what that method must return. Pay particular attention to displays inside containers, stable dictionary keys, and the difference between an operation and its reflected form.
4.1 Give objects readable string representations
An object is easier to inspect when it can describe its own data. Special methods let Python request that description through familiar actions such as print, str, and repr. Their names start and end with two underscores, often called dunder names.
Choose text for two audiences
__str__ supplies readable text for a program's users. __repr__ supplies a precise description for programmers. Both return strings; neither should print its result as a substitute for returning it.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # (3, 4)
print(repr(p)) # Point(3, 4)
str(p) returns the string "(3, 4)". repr(p) returns "Point(3, 4)". When possible, a representation resembles a class call that would recreate the value. It still is text: calling repr does not itself create another point.
Without a custom representation, a plain instance typically displays a class name and an address-like identifier. That helps distinguish objects but does not show their useful data. You rarely call these special methods directly; define them once and use Python's ordinary display operations.
Predict what a container displays
| Expression | Representation requested |
|---|---|
print(p) or str(p) | __str__ |
repr(p) | __repr__ |
f"{p}" | The usual string form for this class |
f"{p!r}" | __repr__ |
print([p]) | The list's display, using repr for its item |
With the class above, print([Point(3, 4), Point(0, 0)]) shows [Point(3, 4), Point(0, 0)]. The list uses the representations of its elements, even though each element also has __str__.
If you define __repr__ alone, the default __str__ uses it too. Defining __str__ alone does not change repr or container displays. When adding just one display method, __repr__ therefore serves more inspection contexts.
Make string fields unambiguous
For a book that already stores title, author, and year, these methods give different useful views:
def __str__(self):
return f"{self.title} by {self.author} ({self.year})"
def __repr__(self):
return f"Book({self.title!r}, {self.author!r}, {self.year})"
For the book with title "Dune", author "Frank Herbert", and year 1965, the first method gives "Dune by Frank Herbert (1965)". The second gives "Book('Dune', 'Frank Herbert', 1965)". !r asks for the string fields' representations, including quotes.
A body containing only print(self.x) returns None, so str(p) fails with TypeError. Returning an integer fails too. Build the required text, then return that string.
Key Takeaways
__str__supplies user-facing text;__repr__supplies a useful programmer-facing representation.- Containers display their elements with
repr. __repr__can supply the default string form;__str__does not replacerepr.- Both methods must return strings.
- Use
!rin an f-string to show quoted representations of string fields.
4.2 Compare and combine objects with operators
Special methods connect a class to operators and built-in functions. Define only operations with a clear meaning for that type: adding vectors is understandable; adding a player to a team is clearer as team.add_player(player).
Compare values and keep hashes safe
Two separate plain instances compare unequal by default. Add value equality to a point class with:
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
Now two points at (3, 4) compare equal even though their identities differ. in, count, index, and remove on lists can find such equal values. The usual != result follows from __eq__.
NotImplemented is a return value that lets Python try the other operand or a fallback. It is not an exception to raise. When neither operand supports value equality, == falls back to identity; a point and the string "3,4" compare unequal.
Defining __eq__ without __hash__ makes instances unhashable. They cannot then be set members or dictionary keys. Equal objects must have equal hashes, and a key's hash must stay stable. If the compared attributes never change, a matching hash can return hash((self.x, self.y)). Keep mutable value objects unhashable when their equality fields can change.
Define an ordering
__lt__ supplies <. A student class can compare scores with return self.score < other.score. Scores 88, 72, and 95 then sort into 72, 88, 95. min chooses the lowest; max chooses the highest by using > and its reflected < comparison.
With only __lt__, a > b can use b < a. Python does not infer <= or >= from that definition; those need their own methods. If an order is useful only for one task, pass a key function instead:
def by_score(student):
return student.score
ordered = sorted(students, key=by_score)
Return new results from arithmetic
Inside a vector class with x and y attributes:
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, factor):
if not isinstance(factor, (int, float)):
return NotImplemented
return Vector(self.x * factor, self.y * factor)
def __rmul__(self, factor):
return self * factor
Vector(1, 2) + Vector(3, 4) produces a new vector (4, 6). Multiplying the first vector by 3 produces (3, 6), leaving it unchanged. __mul__ handles the vector on the left; __rmul__ supports a number on the left. Python does not assume the two orders mean the same thing.
Supply a meaningful length
For a playlist storing self.songs, define __len__ to return len(self.songs). The result must be a nonnegative integer. len(playlist) then counts songs. In this class, length zero also makes if playlist: false; a custom __bool__ would take priority over length when testing truth.
Key Takeaways
__eq__defines value equality; returnNotImplementedfor unsupported operand types.- Equal hashable objects need equal, stable hashes.
__lt__supports sorting but does not supply<=or>=.- Use a key function for a task-specific sort order.
- Arithmetic methods can create new objects without changing the operands.
__rmul__supports multiplication with the object on the right.__len__returns a nonnegative integer and can supply truthiness when no__bool__exists.
5Build programs from objects that work together
Larger programs connect objects with different jobs. A cart can ask items for subtotals; one account can ask another to receive a deposit. Trace the references and the sequence of calls so you can see which state changes and where an error stops execution. Then use those ideas to choose a clear design, plan a small class, and test its public behavior in manageable steps.
5.1 Build objects that contain other objects
An object's attributes can refer to other objects. A cart holds items, a library holds books, and a course holds students. Composition builds a larger object from smaller ones through this has-a relationship.
Give each object its own responsibility
class Item:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def subtotal(self):
return self.price * self.quantity
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
if not isinstance(item, Item):
raise TypeError("a cart holds Item objects")
self.items.append(item)
def total(self):
total = 0
for item in self.items:
total += item.subtotal()
return total
The cart owns a separate list created during initialization. Its add method checks the incoming object before appending it. cart.add("pen") raises TypeError instead of storing text that will fail when a subtotal is requested.
Create the items before or during the calls:
cart = ShoppingCart()
cart.add(Item("notebook", 2.50, 4))
cart.add(Item("pen", 1.25, 3))
print(cart.total()) # 13.75
print(cart.items[0].name) # notebook
The subtotals are 10.0 and 3.75; the cart adds them to get 13.75. The chain cart.items[0].name first reaches the cart's list, then its first object, then that object's name.
Delegate work to the object that knows it
The cart asks each item for subtotal(). This is delegation: one object calls another object's method for part of a task. If a bulk discount changes how a subtotal is calculated, that rule belongs in Item.subtotal. The cart can keep adding the returned subtotals without duplicating price rules.
Passing Item instead of Item(...) passes the class, not a purchased item. The type check rejects that class object. Keep all additions on the checked path; direct access to cart.items.append(...) would bypass add.
Search for an object
Add this method inside ShoppingCart:
def find(self, name):
for item in self.items:
if item.name == name:
return item
return None
The first match returns the existing item. No match returns None. Check found is None before using found.name; None has no such attribute. A __len__ method returning len(self.items) could also make len(cart) count stored item objects, while __str__ could join item lines into a receipt.
Track references inside collections
A list stores references, so placing the same item in two carts does not copy it. Updating that item's price affects the next total calculated by both carts. Separate cart lists prevent accidental list sharing, but they do not make the objects inside independent. Create separate items when the carts must hold independent data.
Key Takeaways
- Composition stores objects inside another object.
- Delegation asks a contained object to perform its part of a task.
- Check an object before storing it when later methods require its type.
- A search can return the matching object or
None. - Separate containers can still refer to the same contained object.
5.2 Make objects interact through methods
Objects can be method arguments and return values just like numbers or strings. Track the receiver, arguments, and result separately: a method may change an existing object, return a new object, or simply report a value.
Use another object as input
Inside a point class with x and y, a method can read both points:
def midpoint(self, other):
return Point((self.x + other.x) / 2,
(self.y + other.y) / 2)
For p = Point(3, 4) and q = Point(1, 0), middle = p.midpoint(q) creates a third point at (2.0, 2.0). During the call, self refers to p and other refers to q. The method calculates coordinates and constructs a new point; it assigns nothing to either original.
With import math, a distance method can return math.hypot(self.x - other.x, self.y - other.y). Point(0, 0).distance_to(Point(3, 4)) then gives 5.0. This call returns a number, while midpoint returns an object.
Let each receiver manage its changes
Inside an account class with checked withdraw and deposit methods:
def transfer_to(self, other, amount):
self.withdraw(amount)
other.deposit(amount)
If Maya has 100 and Leo has 20, maya.transfer_to(leo, 30) first withdraws from Maya, leaving 70, then deposits into Leo, giving 50. In the first call, self is Maya's account. In the deposit call, its own self is Leo's account.
A later transfer of 500 fails in withdraw because Maya has only 70. Execution stops before deposit, so both balances keep their previous values. The order of the calls matters. Python does not undo an earlier state change if a later call raises; this short method depends on the receiver's deposit being able to accept the amount.
Calling other.deposit(amount) runs the receiving account's rules in one place. Directly changing other.balance would skip them.
Trace the objects, not just the variable names
Draw each instance as a box containing its attributes. Draw an arrow from each variable to its box. At a call, identify which box receives self and which boxes are passed as arguments. Update the affected box after each successful state-changing line. Two arrows to one box mean a change through either name reaches the same object.
A plain function can also work across objects. total_balance(accounts) can loop over accounts and add their balances. That whole-list calculation need not belong to a single account.
Represent an absent relationship
A book can start with self.borrower = None, then later store a member object. Before reading a member attribute, check the relationship:
if book.borrower is not None:
print(book.borrower.name)
Otherwise book.borrower.name fails when the book has no borrower. Classes can be defined above a main() function that creates and connects the objects. Calling main() under if __name__ == "__main__": keeps those actions from running when the classes are imported.
Key Takeaways
- Identify the receiver as
selfand the object argument as its own parameter. - A method can return a new object while leaving its inputs unchanged.
- Calls happen in order; an exception stops later calls but does not undo earlier changes.
- Use another object’s public methods to apply its rules.
- Check for
Nonebefore following an optional object relationship. - A function can organize calculations that span several objects.
5.3 Decide when a class makes a program clearer
A class earns its place when related data and repeated operations become easier to understand together. You do not need to turn every function or record into a class. Choose the structure that makes the program's responsibilities clear.
Look for data and behavior that travel together
Classes help when several functions keep accepting the same group of values, many records share a layout, and state changes must follow common rules. Several accounts that all support deposits and withdrawals are a good fit. A single calculation can stay a function. A fixed, read-only record can stay a tuple or dictionary.
A class with only setup and a one-time operation often adds more ceremony than useful structure. Ask what one instance represents and which repeated actions belong to it. If you cannot answer those questions simply, reconsider the design.
Move an operation next to its data
A dictionary-based account might begin as:
account = {"owner": "Maya", "balance": 50}
def deposit(account, amount):
account["balance"] += amount
The class version can place the same operation beside its state and its rules:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self.balance += amount
def __repr__(self):
return f"BankAccount({self.owner!r}, {self.balance})"
After acct = BankAccount("Maya", 50) and acct.deposit(25), its representation is BankAccount('Maya', 75). The positive-deposit rule now runs whenever callers use that method. Reading account["balence"] would raise KeyError; reading acct.balence raises AttributeError. A class does not eliminate spelling mistakes, but it gives the data and its operations a named home.
Plan from a short description
For “A library lends books to members. A member can borrow at most 3 books,” nouns suggest Book, Member, and Library. Verbs suggest checkout and return methods.
- List what each instance knows: a book's title, author, and borrower; a member's name and borrowed books; a library's books and members.
- Identify rules: a book cannot already be on loan, and a member cannot exceed
MAX_LOANS = 3. - Put each rule in the operation that changes the relationship, such as
Library.check_out(book, member). - Write initialization and a useful representation, then add and test one action at a time.
Different titles need different instances of Book, not separate classes named after each title. Use singular nouns for individual objects, such as Book; name a collection for the collection, such as Library. Keep each class's job clear enough to describe in one sentence.
Test observable behavior
acct = BankAccount("Maya", 50)
acct.deposit(25)
assert acct.balance == 75
assert raises AssertionError if its condition is false. Test rejected actions too: catch the expected exception and verify that the state did not change. Use explicit validation with raise for caller input; assertions are checks during development and can be disabled when Python runs with optimization.
Key Takeaways
- Choose a class when related state and repeated operations benefit from one home.
- Keep a simple calculation as a function when no lasting object state is needed.
- Plan attributes from the data and methods from the actions and rules.
- Model different data values as instances of one class.
- Test both successful updates and rejected changes.
Python Classes and OOP examples
1From familiar values to your first class
11 steps
- 1IntroductionStart with objects you know
A list, a string, and a number are all objects. Their types determine which operations they support, and each value has its own identity.
Begin by tracking a string method and a list method. Then use the same ideas to make a simple class whose instances store different data. Keep asking two questions: which object does this name reach, and what does this call return?
- 2ExampleAsk a value about its type
Python can tell you what kind of object a value is.
text = "river" values = [4, 2] print(type(text)) # <class 'str'> print(isinstance(values, list)) # TrueThe string and list offer different methods because they have different types. You can use
dir(values)to inspect the names available on the list, including ordinary methods and special methods. - 3ExampleTrack a returned string
A string method can produce a new value without replacing the original.
name = "robin" result = name.title() print(result) # Robin print(name) # robintitle()returns a new string. The assignment stores that result underresult, whilenamestill refers to"robin". Writingname = name.title()would instead makenamerefer to the returned string. - 4ExampleTrack an in-place list change
Compare the string call with a list sort.
values = [4, 2] result = values.sort() print(values) # [2, 4] print(result) # NoneThe list itself changes, and the return value is
None. Assigning the call back tovalueswould replace that name’s list reference withNone. Usevalues.sort()alone when you want to change the existing list. - 5ChallengeWhere does the append appear?
Predict both printed lists.
a = [7] b = a c = [7] b.append(2) print(a, c)---reveal---
The output is
[7, 2] [7].aandbrefer to one list, so appending throughbchanges the list seen througha.cholds a separate list. The lists were initially equal, buta is cwas false. - 6TipDraw references before tracing changes
Draw one box for each separately created list. Point both
aandbto the first box, and pointcto the second. An append changes the box; assignment changes where a name points.Also distinguish a method from its call:
name.titlegives a method object, whilename.title()runs it. Parentheses can be the difference between holding behavior and obtaining its result. - 7AnalogyOne form, several filled-in copies
Think of a class as a blank form describing the information one kind of object holds. An instance is a filled-in copy with its own data.
A
Badgeclass can represent every badge in a program. One instance holds a visitor’s name; another holds a different name. The class describes the shared kind of object. You do not need a new class for every value written on the form. - 8ExampleDefine the type and make two instances
Use a minimal class while learning how instances work.
class Badge: pass first = Badge() second = Badge() first.name = "Rae" second.name = "Ivo"passprovides an empty body. Each class call creates an instance, and the dot assignments store data on the selected object.first is secondis false.first = Badgewould instead give the class another name without creating an instance. - 9ChallengeFind a misspelled attribute
Continue with the two badges:
first.nmae = "Tess" print(first.name, second.name)What appears?
---reveal---
The output is
Rae Ivo. The assignment creates a separate attribute namednmae; it does not changename. Reading an attribute that does not exist raisesAttributeError, but assigning a new name is allowed. Check the spelling on both sides of a surprising read. - 10ChallengeCombine instances and aliases
Continue with
firstandsecondfrom the badge example:copy_name = first copy_name.name = "Noor" print(first.name, second.name)---reveal---
The output is
Noor Ivo.copy_nameis an alias for the first badge, so the assignment changes that object. The second badge remains separate. Creating instances controls how many objects exist; assigning names controls which objects those names reach. - 11RecapTrace one more program on paper
Try a short program with two class calls and a third name assigned to the first instance. Give the objects different attributes, then change one through the alias.
Before running it, draw the object boxes and predict every read. If a prediction differs from the output, inspect the attribute spelling and the arrows from names to objects. This separates a setup mistake from a reference mistake.
2Build objects with dependable starting data
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionSet up a task collection
- 2ExplanationGive Python an initializer
- 3ExampleMatch a call to its parameters
- 4ChallengeSpot the missing attribute
- 5ExampleAccept an optional starting list
- 6TipCheck how far the copy goes
- 7ExplanationAdd data about the class
- 8ExampleFollow a shadowing assignment
- 9ChallengeRepair the shared counter
- 10ExplanationReject a limit that cannot work
- 11ExampleChoose the error and recovery path
- 12ChallengeDoes printing reject setup?
- 13ChallengeKeep the earlier object on failure
- 14RecapCheck setup as a caller
3Control state through methods and properties
16 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionMake a stock counter useful
- 2ExampleStart with stored quantity
- 3ExplanationLet the receiver supply self
- 4ChallengeMake a result reusable
- 5ExampleAdd stock after checking the amount
- 6ExampleReject a removal before subtraction
- 7ChallengeTrace a change through an alias
- 8ConnectionCopy the right level of state
- 9ExplanationMark internal storage
- 10ExampleRead through a property
- 11ExampleAllow checked public assignment
- 12ChallengeAvoid a recursive setter
- 13Key InsightCompute a value when it is read
- 14ChallengeCombine a setter with an alias
- 15TipRead the definition and the call together
- 16RecapTest one operation at a time
4Make objects fit Python expressions
17 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionChoose what an object should mean
- 2ExampleBegin with a point
- 3ExampleAdd text for inspection
- 4ChallengePredict a list’s display
- 5TipReturn text in both display methods
- 6ExampleCompare coordinate values
- 7Key InsightLeave unsupported comparisons to Python
- 8ChallengeCan equal pairs go in a set?
- 9ExampleChoose a coordinate ordering
- 10ConnectionOrder can belong to one task
- 11ExampleAdd without changing the operands
- 12ExampleSupport both multiplication orders
- 13ChallengeSeparate a new result from an alias
- 14ExplanationUse length for a collection
- 15ChallengePredict the empty queue branch
- 16TipKeep the meaning visible
- 17RecapCheck each expression separately
5Connect objects with clear jobs
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionBuild an order from smaller parts
- 2ExampleGive one line its own calculation
- 3ExampleStore lines in an order
- 4ExampleDelegate each cost
- 5ChallengeTrace a line shared by two orders
- 6TipGuard the missing search result
- 7ExplanationPass an object into another method
- 8ExampleFollow the receiver and result
- 9ChallengeWhere does a failed combination stop?
- 10ConnectionTrace calls that change two objects
- 11ExplanationDecide which behavior belongs together
- 12ChallengeAvoid a class for every product
- 13ExampleTest the checked public entry point
- 14ChallengeChoose a test for the combined design
- 15RecapPlan a small collaboration of your own
Python Classes and OOP practice questions
Set 1: Objects, references, and dependable setup10 Practice Questions
Question 1 of 10
What does this code print?
a = [6, 2]
b = a
a = sorted(a)
b.append(9)
print(a, b)
Show answerHide answer
sorted(a) creates a new sorted list, and the assignment makes a refer to it. b still refers to the original list, so appending changes that list to [6, 2, 9]. Keeping both names on the same list misses the reassignment of a.Question 2 of 10
What does first.name contain at the end?
class Badge:
pass
first = Badge()
second = Badge()
first.name = "Rae"
second.name = "Ivo"
first.nmae = "Sol"
Show answerHide answer
first.name was assigned "Rae" and is never assigned again. The misspelled nmae creates a separate attribute on first, while second.name belongs to another object. A new attribute assignment is allowed, so the typo does not itself raise an error.Question 3 of 10
What does this code print?
label = "cedar"
label.upper()
print(label)
Show answerHide answer
upper() returns a new string, but this call does not store that result. The name label still refers to the original string, so the output is cedar. Expecting CEDAR treats the string method as an in-place update; strings are immutable.Question 4 of 10
Show answerHide answer
Question 5 of 10
What does this code print?
class Counter:
def __init__(self, start=4):
self.value = start
c = Counter(9)
print(c.value)
Show answerHide answer
self and passes 9 as start. The initializer stores that value in self.value, so the output is 9. The default 4 is used only when the caller leaves out start; it does not replace an explicit argument.Question 6 of 10
Why does the final line fail?
class Lamp:
def __init__(self, brightness):
brightness = brightness
lamp = Lamp(7)
print(lamp.brightness)
Show answerHide answer
brightness = brightness reassigns the local parameter and never creates an attribute on the lamp. The read therefore raises AttributeError; setup should store self.brightness = brightness. Returning the number from __init__ would cause a separate TypeError, not create the attribute.Question 7 of 10
What does this code print?
class Folder:
def __init__(self, tags=[]):
self.tags = tags
a = Folder()
b = Folder()
a.tags.append("urgent")
a.tags = []
print(b.tags, a.tags)
Show answerHide answer
"urgent" in that shared list; the later assignment gives only a a new empty list. Expecting two empty lists confuses replacing one attribute reference with clearing the original shared object.Question 8 of 10
What does this code print?
class Ticket:
fee = 3
x = Ticket()
y = Ticket()
x.fee = 8
Ticket.fee = 5
print(x.fee, y.fee)
Show answerHide answer
x.fee creates an instance attribute that shadows the class value. y has no fee of its own, so after the class update it finds 5 on Ticket. Expecting 5 5 overlooks the separate value stored on x, which the class assignment does not replace.Question 9 of 10
What does this code print?
class Job:
count = 0
def __init__(self):
self.count += 1
a = Job()
Job.count = 10
b = Job()
print(a.count, b.count, Job.count)
Show answerHide answer
a reads class count 0 and stores instance count 1. After the class changes to 10, creating b stores its own 11; neither initializer updates Job.count. The result 1 11 11 treats the augmented instance assignment as a class counter update.Question 10 of 10
What does this code print?
class Batch:
def __init__(self, size):
if size <= 0:
raise ValueError("size must be positive")
self.size = size
batch = Batch(6)
try:
batch = Batch(0)
except ValueError:
pass
print(batch.size)
Show answerHide answer
Batch(0) raises before it returns, so the assignment to batch never takes place. The exception is caught, and batch still refers to the earlier valid object with size 6. A failed creation call does not replace the caller’s existing reference with a partial result.Set 2: Methods, state changes, and properties10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What does this code print?
class Tank:
def __init__(self):
self.level = 5
def add(self, amount):
self.level += amount
a = Tank()
b = a
result = b.add(4)
print(a.level, result)
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
self when the program calls panel.area()?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
What change lets the caller calculate double?
class Pace:
def __init__(self, minutes):
self.minutes = minutes
def seconds(self):
print(self.minutes * 60)
p = Pace(3)
double = p.seconds() * 2
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
"12,14"?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
What does this code print?
import copy
class Log:
def __init__(self):
self.entries = []
a = Log()
b = copy.copy(a)
b.entries.append("open")
b.entries = ["closed"]
print(a.entries, b.entries, a is b)
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
name?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Why does assigning to reading.value repeatedly call this setter?
class Reading:
@property
def value(self):
return self._value
@value.setter
def value(self, number):
self.value = number
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
remove(n) method must reject nonpositive amounts and removals larger than the current value. Which sequence leaves the state unchanged on either rejection?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
What does this code print?
class Dial:
def __init__(self, value):
self.value = value
@property
def value(self):
return self._value
@value.setter
def value(self, number):
if not 0 <= number <= 9:
raise ValueError("out of range")
self._value = number
d = Dial(4)
try:
d.value = 12
except ValueError:
pass
print(d.value)
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
area with a read-only property from width and height. An object has width 6 and height 2; its width is then assigned 8. What does reading obj.area return?Show answerHide answer
Save this lesson to see this answer.
Set 3: Readable displays and useful operators10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
__str__ and __repr__. Which method supplies an instance’s text when it appears inside a printed list?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
What does this code print?
class Tag:
def __init__(self, text):
self.text = text
def __repr__(self):
return f"Tag({self.text!r})"
t = Tag("ok")
print(t)
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
str(marker) raise TypeError when its __str__ method only calls print(self.code)?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Label class defines equality by its text and defines no __hash__. Two labels contain the same text. What happens when they are put in a set?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Mark class defines only __lt__, comparing numeric scores, and no other custom comparisons. For a with score 2 and b with score 7, what happens?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Vector defines __mul__ for vector * number but no reflected multiplication method. What method should be added to support number * vector too?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
__eq__ using code and __hash__ as hash(self.code). Its objects are used as dictionary keys. What design change makes the key behavior reliable?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Queue class defines __len__ as the number of pending jobs and defines no __bool__. With no pending jobs, what are len(q) and bool(q)?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
What does the final line print?
class Offset:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Offset(self.value + other.value)
a = Offset(6)
b = a
a = a + Offset(4)
print(a.value, b.value, a is b)
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Rank.__lt__ orders records by priority. One report must instead sort those records by creation time without changing their usual order. What approach fits?Show answerHide answer
Save this lesson to see this answer.
Set 4: Objects that collaborate and designs that stay clear10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What total does the code print?
class Line:
def __init__(self, rate, count):
self.rate = rate
self.count = count
def cost(self):
return self.rate * self.count
x = Line(4, 3)
a = [x]
b = [x, Line(2, 5)]
a[0].count = 6
print(sum(line.cost() for line in b))
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Workshop stores Tool objects in self.tools. What does workshop.tools[0].label read?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
line.cost() on each stored line. A volume discount changes how each line’s cost is calculated. Where should that rule go?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
find(name) returns an object on success and None on failure. What caller code handles both cases before reading result.name?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Position.midpoint(other) method returns a new position whose coordinates are the averages. If a is at (2, 8) and b at (10, 4), what does c = a.midpoint(b) leave?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
send_to(other, n) calls self.remove(n) and then other.add(n). remove rejects an amount above the sender’s stock before changing anything; add accepts positive amounts. Starting with sender stock 9 and receiver stock 4, a send of 3 succeeds, then a send of 8 is attempted and its exception is caught. What stocks remain?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
add method. The receiver rejects the amount before changing its own stock. No rollback code exists, and the caller catches the exception. What state remains?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
The shelf must reject a third book and keep its two existing books. After the caught error below, what assertion exposes the faulty implementation?
class Shelf:
def __init__(self):
self.limit = 2
self.books = []
def add(self, title):
self.books.append(title)
if len(self.books) > self.limit:
raise ValueError("shelf full")
shelf = Shelf()
shelf.add("Atlas")
shelf.add("Birch")
try:
shelf.add("Cove")
except ValueError:
pass
Show answerHide answer
Save this lesson to see this answer.
Set 5: Apply the ideas in new situations10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
What does this code print?
class Kit:
def __init__(self, parts=None):
self.parts = [] if parts is None else list(parts)
source = [[1]]
k = Kit(source)
source.append([2])
source[0].append(3)
print(k.parts)
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
items, what does isinstance(items, list) return?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
What does this code print?
class Tray:
labels = []
a = Tray()
b = Tray()
a.labels.append("red")
b.labels = ["blue"]
Tray.labels.append("green")
print(a.labels, b.labels)
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
_reading. Its initializer assigns directly to self._reading, so Meter(-2) succeeds. What change makes creation use the same check as later public assignments?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Key compares equality only by name, and its hash also uses name. Two different instances start with the name "north"; one becomes a dictionary key. Why must that key’s name remain stable?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
members list, but both lists contain the same Member instance. One club changes that member’s name. The clubs now need independent member names. What change is needed?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
__str__, but a printed list of messages still shows default object descriptions. What addition improves those list entries?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
a.combine(b) is intended to leave both inputs unchanged. Its body calls self.parts.extend(other.parts) and then returns self. What behavior does that implementation actually have?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
class Vault, setup assigns self.__code = "x". What explains why vault.__code fails while vault._Vault__code can work?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
take method uses assert n > 0 as its only check on caller input. The rule must hold even when Python runs with optimization enabled. What change is needed?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Accessor
area() accessor reads its width and height and returns a number without resizing it.Alias
backup = entries, appending through backup also changes the list reached through entries.Assertion
AssertionError if its condition is false; Python can omit it in optimized execution.assert acct.balance == 75 checks behavior; rejected caller input still needs explicit validation.Attribute
book.title = "Emma" stores a title, which can then be read by evaluating book.title.Attribute lookup
p has no own limit, reading p.limit can find the shared limit value defined on its class.B
Backing attribute
score getter reads _score; storing to score inside its own setter would repeatedly call that setter.Behavior test
Bound method
r.area obtains a bound method; adding parentheses as r.area() runs it and returns the calculated area.C
Class
Book class can create many books, each storing a different title while sharing the method definitions.Class attribute
MAX_TOPPINGS setting can supply the default limit read through many different pizza objects.Class design
Composition
Container
cart.items[0] reaches its first stored item.D
Deep copy
copy.deepcopy(account) also copies that list, so appending to the copy’s history leaves the original alone.Default argument
balance=0 in the initializer, Account("Rae") starts with zero without supplying a balance argument.Delegation
Dot notation
cart.items, the name before the dot identifies the object and the name after it selects its items attribute.E
Encapsulation
H
Hashability
__eq__ without __hash__ becomes unhashable, even if its instances work in a list.I
Identity
is operator tests whether two references reach that same object.a == b while a is b is false because their identities differ.Initialization
Instance
Book() and Book() create two separate book instances even though both have the same type.Instance attribute
size and a separate toppings list through assignments on self.Instance method
self.r.area(), Python supplies r to the method so it can multiply that rectangle’s width and height.Internal attribute
balance property while the class’s own methods manage the stored _balance.Invariant
0 and 100 during setup and later assignments.K
Key function
sorted(records, key=by_date) without changing the record class’s usual comparison method.M
Main guard
main() under if __name__ == "__main__": so another program can import the classes without starting it.Method
"cedar".upper() runs a string method, while scores.append(7) runs a method on a list.Mutability
upper() to a string returns a new string.Mutable default
items=[] is stored directly, two objects created without that argument may refer to the same list.Mutator
N
Name mangling
Acc, self.__balance becomes _Acc__balance, which remains accessible under the mangled name.None
None is Python’s single value for an absent or unavailable result, also useful for a relationship with no object assigned yet.borrower = None until a member borrows it, so code checks before reading a borrower’s name.NotImplemented
NotImplemented is a special return value indicating that a comparison or numeric method does not support the supplied operands.NotImplemented for a string, letting Python handle the unsupported pair.O
Object
"hello" and the list [2, 5] are both objects, though their types offer different operations.Object argument
other parameter in p.midpoint(q) refers to q, whose coordinates the method reads to calculate its result.Operator overloading
__add__ can make a + b produce a new vector formed by adding their corresponding coordinates.P
Programmer representation
__repr__, used for inspection and inside container displays."Book('Emma', 'Jane Austen', 1815)", including quoted text fields.Property
student.score can call a getter, and assigning student.score = 90 can invoke a validating setter.R
Receiver
self argument.sender.transfer_to(receiver, 5), sender receives the transfer call; the account argument is the destination.Reference
Reflected operation
3 * vector, Python can use the vector’s __rmul__ when integer multiplication cannot handle that vector.Representation fallback
__repr__ when a class defines no custom __str__.__repr__ can improve both print(point) and the point’s appearance inside a printed list.Responsibility
Return value
r1.area() + r2.area() add results, while printing alone leaves None as each result.S
Search result
None indicating that no match was found.found is not None before reading found.name from a cart’s item search.self
self conventionally receives the current instance, letting a method read or change that object’s attributes.self.owner = owner, the left side stores data on the instance and the right side reads a local parameter.Setter
score setter that rejects 105 before assigning _score leaves a previously valid score unchanged.Shadowing
p.limit = 8, changing Pizza.limit does not replace the separate limit already stored on p.Shallow copy
copy.copy(account) can still share that list with the original.Special method
__str__ when converting an object to its readable string form and __len__ for its length.State
String representation
__str__, normally intended for users of the program."(3, 4)" when printed directly, leaving its numeric coordinate attributes unchanged.T
Truthiness
__bool__, a defined zero length makes it false.__len__ and no custom __bool__ causes if playlist: to skip its body.Type
type([2, 5]) returns the list class, and isinstance([2, 5], list) confirms that the value is a list.TypeError
TypeError reports that an operation received an inappropriate type or was called with arguments its definition does not accept.TypeError because that comparison cannot order text and zero.V
Validation
Value equality
__eq__, instead of requiring the same object identity.ValueError
ValueError reports that an argument has an acceptable type but its value is outside what the operation accepts.-2 can raise ValueError with the message "width must be positive".#
__init__
__init__ method initializes an instance after Python creates it and must return None when initialization completes.Counter(5) passes the new instance and 5 to its initializer before returning the configured counter.__lt__
__lt__ method implements less-than comparison, supplying an ordering that operations such as sorted can use.__lt__ lets a list of student objects sort from lowest score to highest score.About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.