Embercode
✕
Object-Oriented ProgrammingLesson 1 of 17

45 min read

Classes, objects and __init__

Why it matters

Real programs model things: a user, an order, a bank account, a game character. Each of these has data (a name, a total, a health value) and actions (log in, add an item, take damage). A class lets you keep the data and the actions for one kind of thing in one place. Most Python programs you will write for the rest of this course, and many job interviews, are built on classes. This lesson is the starting point for all of it.

The idea from first principles

Before classes, a program was mostly a list of variables and a list of functions that worked on them. If you wanted to track three dogs, you needed three names, three ages, and a function to print each one. Every function had to be told which dog to use. Nothing tied a name to its age, so it was easy to mix them up.

Object-oriented programming fixes this by grouping. You describe one kind of thing once, and then you make as many copies of it as you need. Each copy keeps its own data.

A class is the description. It is a blueprint. An object (also called an instance) is one real thing built from that blueprint. Everyday analogy: a cookie cutter is the class and each cookie is an object. Every cookie has the same shape, because they all came from one cutter. Each cookie can still get its own icing, so the icing belongs to the cookie, not to the cutter.

A class has three kinds of parts:

  • Attributes are variables that hold the data of each object. An attribute belongs to one object. Two dogs have two name attributes with two different values.
  • Methods are functions defined inside the class. A method can read and change the attributes of the object it belongs to.
  • The __init__ method is a special method that runs once when an object is created. Its job is to set the starting values.

Here is the smallest useful class. It has two attributes, one __init__ method and one more method.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
    def bark(self):
        print(self.name + " says woof")

Read it as a plan. It says: every dog has a name and an age. When you make a dog, give it a name and an age. Any dog can bark, and when it barks it uses its own name.

The class does not run by itself. Nothing happens until some code creates an object. In Python you create an object by calling the class as if it were a function. The code Dog("Rex", 3) creates a dog.

Two words in the example need a plain explanation. self is the first parameter of every method. It stands for the object the method runs on. self.name means "the name attribute of this object". The name self is only a convention. Python does not force you to use it, but nearly every Python programmer does.

What happens when you call a class

To understand classes, you need a simple picture of how Python handles names and objects. A variable in Python is a name that points to an object. Python does not copy the object when you assign it to another name. It attaches a second name to the same object.

When Python runs this line:

a = Dog("Rex", 3)

it does four things, in order:

  1. It creates a new, empty object of type Dog.
  2. It calls __init__ on that new object, passing the object as self and the values "Rex" and 3 as name and age. __init__ sets self.name to "Rex" and self.age to 3.
  3. It returns the new object.
  4. It attaches the name a to that object.

You can think of the name a as a sticky note with an address written on it. The note is small. The object it points to holds the data. Python keeps track of which objects are still reachable. When nothing points to an object anymore, Python frees it. In CPython, the standard implementation of Python, each object keeps a count of how many names point to it, and the memory is freed when that count reaches zero.

If you write a second line, b = Dog("Milo", 1), Python builds a second object. Now a and b point to different objects. Changing a.age never changes b.age, because they are different objects with their own attributes.

The key idea in this lesson is this: calling the class always makes a new object, and the name only points to it.

Worked example 1: two dogs and a bark

Here is a complete program that uses the Dog class. It is the first runnable program in this module. Click Run to execute it. You do not need to type any input for this one.

Python · editable

Trace it line by line:

  1. The class Dog is defined. Python now knows what a dog looks like. Nothing is created yet.
  2. Dog("Rex", 3) creates the first object. __init__ runs with name set to "Rex" and age set to 3. The name a points to it.
  3. Dog("Milo", 1) creates a second object. b points to it.
  4. a.bark() runs the bark method on the object that a points to. Inside the method, self.name means a's name, which is "Rex". The line prints Rex says woof.
  5. b.bark() runs the same method on the other object. Inside it, self.name means "Milo". The line prints Milo says woof.
  6. a.age + b.age is 3 + 1, which is 4. The line prints 4.

The output is:

Rex says woof
Milo says woof
4

The method body was written once, but it ran twice and printed two different names. That is the point of a method. It is written once in the class, and each object runs it on its own data.

The dot in a.age means "the attribute age of the object that a points to". The dot in a.bark() means "run the method bark on that object". You will use the dot more than any other symbol in Python classes.

Each object keeps its attributes in a dictionary that Python stores on the object. You can look at it with a.__dict__. Running print(a.__dict__) after the program above shows {'name': 'Rex', 'age': 3}. You will rarely need this in real code, but it shows that the attributes really are stored per object.

Worked example 2: default values instead of several constructors

Python does not allow two __init__ methods in one class. The second definition would replace the first. Python handles this with default values for parameters. A parameter with a default can be left out when the object is created. (Readers who know Java: Python has no constructor overloading.)

Here is a bank account. The __init__ method takes an owner and a balance. Both have defaults, so the account can be created with no arguments, with only an owner, or with both.

Python · editable

The output is:

unknown: 0
Asha: 100
Ravi: 250
Ravi: 250 (frozen)
None 0 False

Trace the three calls:

  • Account() has no arguments. Both parameters use their defaults, so owner is "unknown" and balance is 0.
  • Account("Asha", 100) passes both values by position. The first value goes to owner, the second to balance.
  • Account("Ravi", 250) works the same way with different values.

The line self.frozen = False sets an attribute that the caller does not pass in. Every account starts unfrozen. Later, c.frozen = True changes that one object. Then describe prints the extra text. The conditional " (frozen)" if self.frozen else "" gives the text when frozen is true and an empty text when it is false. You will see this form again in the conditionals module.

You can also pass arguments by name. Account(balance=250, owner="Ravi") gives the same object as the third call above. Named arguments make a call easier to read when a function has several defaults.

The Blank class has no __init__ at all. Python gives every class a default __init__ that takes no arguments and does nothing. The class still works, because you can add attributes to any object at any time. Here the three attributes are set after the object is created. Python does not stop you.

Attributes that were never set

Python does not give attributes a default value. An attribute that was never set does not exist. Reading it raises an error.

class Blank:
    pass
 
x = Blank()
print(x.label)

The output is:

Traceback (most recent call last):
  File "dog.py", line 5, in <module>
    print(x.label)
          ^^^^^^^
AttributeError: 'Blank' object has no attribute 'label'

The file path in the first line depends on where you save the file, so this output shows the name dog.py. The error type is AttributeError. It means "you asked for an attribute that this object does not have". The last line names the class, Blank, and the missing name, label.

The fix is to set every attribute in __init__, so that every object has all of its attributes from the start. The Account example does it with self.frozen = False.

Constructors in depth

In Python the object is built in two steps. First, the class creates an empty object. Then __init__ fills it in. The name __init__ is short for "initialize". The step that creates the empty object is a separate method called __new__, but beginners almost never write it. For this lesson, think of __init__ as the constructor.

__init__ has three rules that you should know:

  1. Its name is exactly __init__, with two underscores on each side.
  2. Its first parameter is self. Python passes the new object in for you, so you never pass it yourself.
  3. It must return None. You do not write a return statement, so this happens by default. If you return any other value, Python raises an error. You will see this in the mistakes section.

__init__ runs once for each object, at the moment the object is created. You cannot call it again on an existing object to reset it. If you want to reset an object, write a method for that.

Passing one initializer to another

Python does not need a way to call one __init__ from another. The default values in one __init__ cover most of the cases. When the logic is more complex, you can write a regular method and call it from __init__, or create the object in a class method. Class methods are an advanced topic. You will meet them in the factory lesson.

The self parameter

Inside any method, self means "the object this method is running on". It is a reference to the current object, the same kind of thing as a or b in the earlier example. You need it to read or write any attribute from inside the class.

class Dog:
    def __init__(self, name, age):
        self.name = name   # left: the attribute of this object; right: the parameter
        self.age = age

Without self., the name name refers to the parameter only. The line name = name would store the parameter in a local name that disappears when __init__ ends. The object would never get an attribute called name. Python does not complain about this line when the class is defined, so it is a silent bug. It is one of the most common beginner bugs, and it appears in the mistakes section below.

Some programmers choose different parameter names, such as n and a, to avoid the repeated words. Both styles exist. Using self.name = name makes the link between the parameter and the attribute obvious, and most Python style guides recommend it.

Two names, one object

Assigning one name to another copies the reference, not the object. Both names then point to the same object. This matters a lot when you read or pass objects.

Python · editable

The output is:

10
5
True
False
False

Trace it:

  • a points to the first counter, which starts at 5.
  • b = a makes b point to the same counter. There is still only one counter object in the first pair.
  • c points to a separate counter, also starting at 5.
  • b.value = 10 changes the object that b points to. That object is also the one a points to. So a.value is now 10.
  • c.value is still 5, because c never touched the first object.
  • a is b checks whether the two names point to the same object. They do, so the answer is True.
  • a is c is False, because they point to different objects.
  • a == c is also False. For a class that does not define a comparison method, == checks whether the two names point to the same object, the same as is. Two separate objects with the same values are not equal until you define how they compare. You will learn how to do that in the operator overloading lesson.

Beginners often think b = a makes a copy. It does not. If you want two independent objects, you must call the class again, or use the copy module, which the variations section shows.

Passing objects to functions

Python passes arguments by object reference. Many people call this "call by sharing". The function receives its own name for the same object. That name points to the same object as the caller's name. The next example shows what this allows and what it does not.

Python · editable

The output is:

after grow: 8
inside replace: 99
after replace: 8

grow changes the object through its name. The caller sees the change, because both the caller and grow point to the same box. The box's size goes from 4 to 8.

replace assigns a new box to its own name b. That changes only the local name, so the caller still points to the original box. The line inside replace prints 99, but when replace returns, the new box is lost, and box.size is still 8.

The rule to remember is simple. A function can change the object it receives. It cannot make the caller's name point somewhere else.

Reading input and building objects

Real programs build objects from data. Often that data comes from a file, a web form or the user. The next program reads a count and then that many pairs of values. Each pair becomes one Dog, and the program keeps the oldest one.

To run this program, type the following into the Input box, with each line on its own line:

3
Rex 3
Milo 7
Pip 1

The first number is how many dogs follow. Each dog line has a name and an age, separated by a space. input() reads one line as text. int(...) turns text into a whole number. split() cuts a line into words at the spaces.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
 
n = int(input())
dogs = []
for _ in range(n):
    name, age = input().split()
    dogs.append(Dog(name, int(age)))
 
oldest = dogs[0]
for d in dogs:
    print(f"{d.name} is {d.age}")
    if d.age > oldest.age:
        oldest = d
print("Oldest:", oldest.name)

The output is:

Rex is 3
Milo is 7
Pip is 1
Oldest: Milo

What happens inside:

  • dogs = [] makes an empty list. A list holds objects in order, and it can grow.
  • Each loop turn creates one Dog and adds it to the list. The list holds the names of the objects, not copies of them.
  • oldest = dogs[0] starts with the first dog. It attaches a second name to the same object, so no dog is copied.
  • The second loop walks through every dog. When it finds an older one, it moves oldest to point to that dog. Only the name moves. No dog is copied or changed.

A list of objects is a pattern you will meet again. Lists, queues and trees in the data structures modules are all built from objects that hold references to other objects.

What Python does for you

Python does less automatic work than Java. Three things are worth knowing.

The first is the default __init__. If a class has no __init__, Python uses one that takes no arguments. So a class with no __init__ can be created only with no arguments:

class Dog:
    pass
 
d = Dog("Rex", 3)

The output is:

Traceback (most recent call last):
  File "dog.py", line 4, in <module>
    d = Dog("Rex", 3)
TypeError: Dog() takes no arguments

TypeError means a value of the wrong kind or number was given to a function or a class. The message says the class takes no arguments. Add an __init__ with the parameters you need.

The second is the compiled form of your code. When you import a module, Python compiles the source to bytecode and stores it in a __pycache__ folder next to the file. You do not need to manage this folder. If you delete it, Python rebuilds it.

The third is that Python does not check types when a class is defined. Python does not stop you from storing a name in an attribute and later a number in the same attribute. Type hints, which you will see later in this course, let tools check this for you. They do not change how the program runs.

How classes are laid out in files

So far every example has put one class or a few short classes in one file. Real projects put related code in separate files, called modules. A module is simply a file that ends in .py. The file name, without .py, is the module name.

To use a class from another file, you write an import line. If dog.py holds the Dog class, then from dog import Dog brings the class into the current file. A folder that contains an __init__.py file is a package. A package groups related modules, and you import from it with dotted names such as from shop.orders import Order.

Many modules end with this pattern:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
 
if __name__ == "__main__":
    d = Dog("Rex", 3)
    print(d.name)

Python sets the name __name__ to "__main__" for the file you run directly. When another file imports this module, __name__ holds the module name instead, so the code under the if does not run. This lets one file hold both a class and a short demo. Python programmers use this pattern a lot.

The access rules in the encapsulation lesson use a naming convention instead of the keywords private and protected from Java. A name that starts with one underscore, such as _balance, means "internal, please do not use from outside". Python does not enforce it. A name that starts with two underscores is changed by Python in a way that makes accidental use harder. You will meet both in the next lesson.

Common mistakes

These mistakes are very common. Each one below shows the real output you will see. The file name in each traceback is dog.py, which is the name these examples were saved under.

Mistake 1: arguments that do not match __init__

Suppose a class has an __init__ with two parameters, and you create an object with one argument.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
d = Dog("Rex")

The output is:

Traceback (most recent call last):
  File "dog.py", line 6, in <module>
    d = Dog("Rex")
        ^^^^^^^^^^
TypeError: Dog.__init__() missing 1 required positional argument: 'age'

How to read it: the last line names the problem. missing 1 required positional argument: 'age' says that the call did not give a value for age. The traceback points to the call on line 6, which is the line that needs a fix.

The fix is to pass what __init__ needs, Dog("Rex", 3), or give age a default value, def __init__(self, name, age=0).

Mistake 2: name = name in __init__

class Dog:
    def __init__(self, name):
        name = name
 
d = Dog("Rex")
print(d.name)

The class is accepted, because the line name = name is valid Python. The object is created without an error. The error comes later, when you read the attribute:

Traceback (most recent call last):
  File "dog.py", line 6, in <module>
    print(d.name)
          ^^^^^^
AttributeError: 'Dog' object has no attribute 'name'

The line name = name assigns the parameter to the name name in the same function. That local name disappears when __init__ ends. The object never got an attribute. The fix is self.name = name.

The error shows up at the line that reads the attribute, which can be far from the line that caused it. That is why the debugging walkthrough below starts from the error and works backwards.

Mistake 3: a method without self

class Dog:
    def __init__(self, name):
        self.name = name
 
    def bark():
        print(self.name + " says woof")
 
d = Dog("Rex")
d.bark()

The output is:

Traceback (most recent call last):
  File "dog.py", line 9, in <module>
    d.bark()
    ~~~~~~^^
TypeError: Dog.bark() takes 0 positional arguments but 1 was given

Python passes the object as the first argument to every method call. The method bark() was written with no parameters, so there is no place to put the object, and the call fails. The fix is def bark(self):. Also, the name self must be written inside the method, because it is the only way to reach the object's attributes.

Mistake 4: thinking b = a copies

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
first = Account("Asha", 100)
second = first
second.balance = 0
print(first.balance)

The output is:

0

second and first point to the same object. The change happened to the one object that both names share. If you need two independent accounts, create the second one by calling the class, or use copy.copy as the variations section shows.

Mistake 5: a mutable default value

A default value is created once, when Python reads the def line. It is not created again for each call. If the default is a list, every object that uses the default shares the same list.

class Team:
    def __init__(self, name, members=[]):
        self.name = name
        self.members = members
 
    def add(self, person):
        self.members.append(person)
 
a = Team("red")
b = Team("blue")
a.add("Ana")
print(b.members)

The output is:

['Ana']

The team blue never received "Ana", but its members list shows her, because both teams share the one list created by the default. The fix is to use None as the default and create a new list inside __init__:

Python · editable

The output is:

[]

Each team now gets its own empty list. The same rule applies to dictionaries and sets used as defaults.

Mistake 6: returning a value from __init__

class Dog:
    def __init__(self, name):
        self.name = name
        return name
 
d = Dog("Rex")

The output is:

Traceback (most recent call last):
  File "dog.py", line 6, in <module>
    d = Dog("Rex")
        ^^^^^^^^^^
TypeError: __init__() should return None, not 'str'

__init__ sets up the object. It must not return a value. Remove the return line.

Debugging walkthrough: following an error

Suppose a program stops with an AttributeError. Use this routine. It works for most errors you will meet in this course.

  1. Read the last line of the message. Note the error type and the text after it. Here it is 'Dog' object has no attribute 'name'.
  2. Read the line number in the traceback. In the example, it is line 6. Open that line.
  3. Find the attribute that is missing. The message names it, here name, and the class, here Dog.
  4. Work backwards. Where did d come from? A call such as Dog("Rex"). Where should name have been set? In __init__. Now the bug is visible: the class does not set the attribute.
  5. Fix the source of the value, not the line that crashed. Adding a check at line 6 would hide the bug.

A useful habit is to print the value before the risky line. A single line like print("name =", d.__dict__) shows which attributes the object has. Remove the print after you find the problem.

Variations and edge cases

An object does not have to hold a value for every attribute. Some attributes are computed from others, and some are not set in __init__ at all. An attribute that is not set raises AttributeError when it is read. Always check that __init__ sets every attribute that the other methods depend on.

A constructor can do more than assign attributes. It can check values, compute derived values, or print a message. Keep __init__ simple, though. An __init__ that does heavy work, such as opening a file or calling a web service, makes creating an object slow and hard to test. Later modules show a better pattern for that work.

Copying an object is a common task. The copy module makes a shallow copy, which means a new object with the same attribute values. Changing the copy's plain attributes does not change the original.

Python · editable

The output is:

100 0

A shallow copy shares any objects stored inside the object. If an attribute holds a list, the copy and the original both point to that one list. copy.deepcopy copies those inner objects too. For a class with lists or other objects inside it, use deepcopy or write your own copy method. The encapsulation lesson returns to this.

Edge case: a class with no attributes and no methods is legal. class Empty: pass creates objects that hold nothing. This is useful when you only need a unique object, for example as a marker value.

Python · editable

The output is:

Empty
True

type(marker) returns the class of the object. .__name__ gives the class name as text. isinstance checks whether an object was made from a class.

Interview-style questions

Interviewers ask about classes in a few predictable ways. Here are the questions with short model answers.

What is the difference between a class and an object? A class is a blueprint that describes attributes and methods. An object is one instance made from that blueprint by calling the class. Each object has its own attribute values, but all objects of a class share the same methods.

What does __init__ do? It sets up a new object so that it is valid from the start. Python calls it once, right after the object is created. It takes self as its first parameter and must return None.

What happens when an attribute is never set? Reading it raises AttributeError. Python does not give attributes a default value the way Java gives fields a default of null or 0.

If you assign one name to another, is the object copied? No. The name is attached to the same object, so both names point to one object. A change through one name is visible through the other.

Can a function change the caller's object? Yes, if it changes the object's attributes through the name it received. No, if it tries to make the caller's name point to a different object, because the function only has its own name for the object.

Why does Python need self? Python has no hidden variable for the current object. The object is passed to each method as the first parameter, and the method uses that parameter to reach the object's attributes.

Why should a default argument not be a list? A default is created once and shared by every call that uses it. Use None as the default and create the list inside the function.

Real-world uses

A web shop has an Order class with the customer, the items and the total. Every order that a customer places is one Order object. Each one has its own items, but the methods to add an item or compute a total are shared. A game has a Player class with a name, a health value and a position. Each player in the game is one object, and the code that moves a player is written once.

In a bank, the Account class you saw above is the core of many features. __init__ makes sure every account starts with an owner and a balance. The attributes hold the balances of millions of accounts, and each one is a separate object in memory.

These examples are all the same idea at different sizes. You describe the thing once, and each real case is an object with its own data.

Practice

Try these before you open the answers. Type your code and run it in the editor when the exercise asks for a program.

Exercise 1: a Book class (easy)

Write a class Book with two attributes, title (a string) and pages (an integer). Give it an __init__ that sets both attributes, and a method describe() that prints the title, a space, and then the pages. Create one book with the title Dune and 412 pages, and call describe().

Expected output:

Dune 412

Show the answer

Python · editable

The __init__ method copies the two parameters into the attributes. The method reads the attributes of the object it runs on.

Exercise 2: a default page count (easy)

Change Book so that pages has a default of 100. Create a book with only a title, Emma, and check that describe() prints 100 for the pages.

Show the answer

Python · editable

The default value lives in one place, the def line. Callers that pass a page count override it.

Exercise 3: predict the output (easy)

Without running it, predict what this prints, then check. Explain why.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
x = Dog("A", 1)
y = x
y.name = "B"
print(x.name)

Show the answer

It prints B. The line y = x attaches a second name to the same object. Both x and y point to that object, so changing y.name changes the object that x also sees.

Exercise 4: count the objects (easy)

A program runs a = Dog("A", 1), then b = Dog("A", 1), then c = a. How many objects exist? What do a == b and a == c print?

Show the answer

Two objects exist. a and c share one object, and b has the other. a == b prints False, because the two names point to different objects, even though the names and ages match. The class does not define a comparison, so == checks identity, the same as is. a == c prints True.

Exercise 5: a counting class (medium)

Write a class Counter with one attribute count, set by __init__ from a starting value. Add a method increment() that adds 1 and a method show() that prints the current count. In your main code, create a counter starting at 10, increment it three times, and show it. Then create a second counter from the first one's value (Counter(first.count)) and increment only the second one. Show both.

Expected output:

13
14

Show the answer

Python · editable

The second counter starts from the number 13, which it gets from first.count. Incrementing the second counter makes it 14 and does not touch first, so the lines are 13 and then 14.

Exercise 6: read a list of objects (medium)

Read an integer n from input, then n lines, each with a name and a score. Store each pair in a Player object with name and score attributes. Print the name of the player with the highest score. If two players tie, print the one that appears first.

Input:

3
Ana 40
Ben 55
Cy 55

Expected output:

Ben

Show the answer

class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
 
n = int(input())
best = None
for _ in range(n):
    name, score = input().split()
    p = Player(name, int(score))
    if best is None or p.score > best.score:
        best = p
print(best.name)

The check best is None handles the first player, because best starts as None, which means "no object yet". The strict > keeps the earlier player when scores tie.

Exercise 7: fix the constructor (medium)

This class is meant to store a product name and price, but the attributes are never set. Find and fix the bug, then explain it in one sentence.

class Product:
    def __init__(self, name, price):
        name = name
        price = price

Show the answer

Both lines assign the parameter to a local name, so the object never gets the attributes. Use self.name = name and self.price = price. The self. tells Python to store the value on the object, not in a local name that disappears when __init__ ends.

Exercise 8: a wallet that stops (medium)

Write a class Wallet with a coins attribute set by __init__. Add a method spend(amount) that subtracts the amount only when coins is at least amount. Otherwise it prints not enough and leaves coins unchanged. Test with a wallet of 10 coins: spend 4, spend 8, then print the coins.

Expected output:

not enough
6

Show the answer

Python · editable

The check runs inside the method, so every caller gets the same protection. Spending 4 leaves 6. Spending 8 fails and prints not enough, and the final balance is 6.

Exercise 9: what does it print? (medium)

Predict the output of this program, then run it.

class Cell:
    def __init__(self, v):
        self.value = v
 
 
def bump(c):
    c.value = c.value + 1
    c = Cell(100)
    c.value = c.value + 1
 
 
x = Cell(1)
bump(x)
print(x.value)

Show the answer

It prints 2. Inside bump, the first line changes the caller's object, so its value becomes 2. The second line makes the name c point to a brand new cell, which is then changed to 101, but the caller never sees that cell. The caller's x still points to the first cell, whose value is 2.

Exercise 10: a list of workers (hard)

Write a program that reads n and then n lines of name hours, and creates a Worker object for each one. Keep the workers in a list. Then print the total hours and the name of the worker with the most hours. Give Worker an __init__ with the parameters and a method overtime() that returns hours - 40 when hours are above 40 and 0 otherwise. Print the sum of all overtime values as the last line.

Input:

4
Ana 38
Ben 45
Cy 52
Di 40

Expected output:

Total hours: 175
Most hours: Cy
Overtime: 17

Show the answer

class Worker:
    def __init__(self, name, hours):
        self.name = name
        self.hours = hours
 
    def overtime(self):
        if self.hours > 40:
            return self.hours - 40
        return 0
 
 
n = int(input())
workers = []
for _ in range(n):
    name, hours = input().split()
    workers.append(Worker(name, int(hours)))
 
total = sum(w.hours for w in workers)
most = max(workers, key=lambda w: w.hours)
overtime = sum(w.overtime() for w in workers)
print("Total hours:", total)
print("Most hours:", most.name)
print("Overtime:", overtime)

Total is 38 + 45 + 52 + 40 = 175. Overtime is 5 + 12 = 17, because only Ben and Cy go over 40. max with key= returns the first worker with the largest value, so ties keep the earlier worker. The expression sum(w.overtime() for w in workers) is a generator expression, a short way to write a loop that feeds sum.

Exercise 11: a constructor with a check (hard)

Write a class Temperature whose __init__ takes a Celsius value as an integer. If the value is below -273, the constructor should store -273 instead (the lowest the scale allows). Add a method warmer(degrees) that returns a new Temperature object degrees higher, without changing the original. Create a temperature of -300, warm it by 50, and print both values.

Expected output:

-273
-223

Show the answer

Python · editable

The check lives in __init__, so no object can ever hold a value below the limit. warmer builds a new object instead of changing t, which keeps the original value available. The new object's __init__ applies the check again. Starting from -273 and adding 50 gives -223.

Exercise 12: an attribute you forgot (hard)

A class Score has three attributes, home, away and winner. __init__ sets home and away. A method decide() sets winner to "home", "away" or "draw". A student writes decide() correctly, but when they print winner before calling decide(), the program stops with an error. Explain why, and say what __init__ should do about it.

Show the answer

__init__ never sets winner, so the object has no attribute with that name until decide() runs. Reading it first raises AttributeError: 'Score' object has no attribute 'winner'. The fix depends on the rule the class should follow. If every Score should have a winner value from the start, set self.winner = None in __init__, or call decide() at the end of __init__. The lesson is that every attribute should be set in __init__, so that the object is complete when __init__ finishes.

What you now know

  • A class is a blueprint with attributes (data), methods (functions that act on the object) and __init__ (setup). An object is one thing made from the blueprint by calling the class.
  • A name holds a reference to an object. Assigning a name to another name attaches a second name to the same object. It does not copy the object.
  • Calling a class creates an empty object, runs __init__ with self set to that object, and returns it.
  • __init__ takes self first and returns nothing. Python has no overloading. Use default values or named arguments instead.
  • Set every attribute in __init__. An attribute that was never set raises AttributeError when read.
  • A function can change the object it receives, but it cannot change which object the caller's name points to.
  • Never use a list or dictionary as a default value. Use None and create the object inside the function.
Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.