Your first class¶
Goal: define a class, create objects from it, and give them attributes and methods, using the to-do task as the example.
Why this matters¶
The to-do app stores a task as a dictionary with four keys. It works, but look at what it costs. Every function that
touches a task must spell "title", "due", "priority" and "done" exactly right, and a slip is a KeyError at
run time. Nothing stops you making a task with three keys, or five. And the functions that only make sense for a task,
such as describe_due(task, today), float around the program with no connection to the thing they describe.
Python has a way of saying this is a kind of thing, here is what it holds, and here is what it can do. It is
called a class, and you have been using classes all along without writing one. str, int, list, date:
each is a class. This lesson shows you how to write your own.
A class is a new type¶
Here is the task, as a class:
from datetime import date
class Task:
def __init__(self, title, due, priority):
self.title = title
self.due = due
self.priority = priority
self.done = False
milk = Task("Buy milk", date(2026, 9, 21), 2)
dentist = Task("Call the dentist", date(2026, 9, 18), 3)
print(milk.title)
print(milk.due)
print(milk.done)
print(dentist.title)
milk.done = True
print(milk.done)
Take the definition line by line.
class Task:starts the definition, the waydefstarts a function. The name is capitalised by convention, so thatTaskis visibly a class andtaskis visibly a variable.- Inside it is a function with the strange name
__init__. Two underscores on each side mark a name that Python itself looks for. This one is run automatically whenever a newTaskis created, and its job is to set the object up. Init is short for initialise. - Its first parameter is
self. When you writeTask("Buy milk", ...), Python creates a new, empty object, then calls__init__with that object asself, followed by the values you gave.selfis simply the object being set up, and every function inside a class has it as its first parameter. self.title = titlestores the value in the object, under the nametitle. This is an attribute, the same kind of thing astoday.yearin Module 8, except that this time you are the one creating it. Four lines create four attributes.doneis not a parameter: every new task starts not done, so__init__sets it itself.
Below the class, Task("Buy milk", date(2026, 9, 21), 2) makes a task. This looks exactly like date(2026, 9, 21),
because it is exactly like it. date is a class, and calling a class makes a new value of that type. A value made
from a class is called an object, and milk and dentist are two objects of the same class, each with its own
attributes. milk.title reads one, and milk.done = True changes one, with no square brackets or quotes anywhere.
Telling Python how to print an object¶
Print a task as it stands and you get something unhelpful:
class Task:
def __init__(self, title):
self.title = title
milk = Task("Buy milk")
print(milk)
That is Python saying a Task object, at this address in memory, because it has no idea what a task should look like as text. You tell it with a second special function:
from datetime import date
class Task:
def __init__(self, title, due, priority):
self.title = title
self.due = due
self.priority = priority
self.done = False
def __str__(self):
if self.done:
mark = "[x]"
else:
mark = "[ ]"
return f"{mark} {self.title} (due {self.due}, priority {self.priority})"
milk = Task("Buy milk", date(2026, 9, 21), 2)
print(milk)
milk.done = True
print(milk)
print(f"Next up: {milk}")
[ ] Buy milk (due 2026-09-21, priority 2)
[x] Buy milk (due 2026-09-21, priority 2)
Next up: [x] Buy milk (due 2026-09-21, priority 2)
__str__ must return a string, and Python calls it whenever it needs the object as text: in print(), in an f-string,
in str(). Inside it, self is the object being printed, so self.title and self.done are that task's own values.
Notice that nothing outside the class changed. print(milk) is the same call as before. The class just knows how to
answer it now.
__init__ and __str__ are the only two of these double-underscore names you need for now. There are others, and
you will meet them when you need them.
Methods: functions that belong to the class¶
You have called methods since Module 2: .upper() on a string, .append() on a list. A method is a function
defined inside a class, and it works on the object it is called on. Here is the task with three:
from datetime import date
class Task:
def __init__(self, title, due, priority):
self.title = title
self.due = due
self.priority = priority
self.done = False
def __str__(self):
if self.done:
mark = "[x]"
else:
mark = "[ ]"
return f"{mark} {self.title} (due {self.due}, priority {self.priority})"
def finish(self):
"""Mark the task as done."""
self.done = True
def is_overdue(self, today):
"""Return True if the task is not done and its due date has passed."""
return not self.done and self.due < today
def describe_due(self, today):
"""Return a short phrase saying when the task is due, or an empty string if it is done."""
if self.done:
return ""
days = (self.due - today).days
if days == 0:
return "today"
elif days == 1:
return "tomorrow"
elif days > 1:
return f"in {days} days"
else:
return f"OVERDUE by {-days} days"
if __name__ == "__main__":
today = date(2026, 9, 20)
milk = Task("Buy milk", date(2026, 9, 21), 2)
dentist = Task("Call the dentist", date(2026, 9, 18), 3)
print(milk.describe_due(today))
print(dentist.describe_due(today))
print(dentist.is_overdue(today))
dentist.finish()
print(dentist)
print(dentist.is_overdue(today))
describe_due() is the function from the end of Module 9, moved inside the class. Two things changed. Its first
parameter is self, and where it used to say task["due"] it now says self.due. The call changed to match:
milk.describe_due(today), and Python passes milk as self, so you give one argument fewer than the def line
has parameters. That is the rule for every method: the object goes before the dot, and everything else goes in
the parentheses.
finish() takes nothing but self and changes the object: self.done = True. After dentist.finish(), the dentist
task is done, and is_overdue() on it returns False, because a done task is never overdue.
The demo code at the bottom is under if __name__ == "__main__":, from Module 8, so that the next program can import
the class without the demo running.
The task, side by side¶
| With a dictionary | With the class |
|---|---|
{"title": "Buy milk", "due": ..., "priority": 2, "done": False} |
Task("Buy milk", ..., 2) |
task["title"] |
task.title |
task["done"] = True |
task.finish() or task.done = True |
describe_due(task, today) |
task.describe_due(today) |
print(task) shows the raw dictionary |
print(task) shows what you chose |
A missing key is a KeyError at the moment of use |
A missing argument is a TypeError at the moment of creation |
| Any function can be handed any dictionary | describe_due() can only be called on a Task |
Nothing on the right is impossible on the left. The difference is that the class puts the shape of a task and the things a task can do in one place, with a name, and Python helps you keep them consistent. The next lesson looks at when that is worth it and when a dictionary is fine.
The table, with objects¶
Here is show_tasks() from Module 9 working on Task objects instead of dictionaries:
from datetime import date
from task_methods import Task
def due_date(task):
"""Return the part of a task to sort by."""
return task.due
def show_tasks(tasks, today):
"""Print the tasks as a table, with a column saying when each one is due."""
print(f"{'#':>2} {'Title':<24}{'Due':<12}{'Pri':>3} When")
number = 1
for task in tasks:
print(f"{number:>2} {task.title:<24}{str(task.due):<12}{task.priority:>3} {task.describe_due(today)}")
number += 1
today = date(2026, 9, 20)
tasks = [
Task("Buy milk", date(2026, 9, 21), 2),
Task("Finish Module 10", date(2026, 10, 3), 1),
Task("Call the dentist", date(2026, 9, 18), 3),
]
tasks[2].finish()
tasks.sort(key=due_date)
show_tasks(tasks, today)
# Title Due Pri When
1 Call the dentist 2026-09-18 3
2 Buy milk 2026-09-21 2 tomorrow
3 Finish Module 10 2026-10-03 1 in 13 days
The class is imported from task_methods.py like any module. The changes from the Module 9 version are all one
kind: task['title'] became task.title, and the When column calls the method. Sorting works the same way as
before, with a key function that returns task.due. Objects can go in lists, be passed to functions, and be sorted,
exactly as dictionaries can, because they are values like any other.
Try it¶
Write a Dog class with a name and an age, a __str__ that says Rex, 3 years old, and a method birthday() that
adds one to the age. Make two dogs, give one of them a birthday, and print both. Then, without running it, predict
what print(Dog) and print(rex.birthday) show, with no parentheses. Run it and see.
Common mistakes¶
TypeError: Task.init() missing 2 required positional arguments: 'due' and 'priority'
You made a Task with too few values. Every parameter of __init__ except self must be supplied when you
call the class. The message lists the ones you left out.
TypeError: Task.describe_due() takes 0 positional arguments but 1 was given
The method was defined without self. Python passed the object anyway, and there was no parameter to receive it.
Every method's first parameter is self.
AttributeError: 'Task' object has no attribute 'titel'
A misspelled attribute, the class version of a misspelled key. Compare it with the names set in __init__.
AttributeError: 'Task' object has no attribute 'done', but I set it in init
You wrote done = False instead of self.done = False. Without self., it is a local variable that vanishes
when __init__ ends. Attributes are always created through self.
NameError: name 'self' is not defined
self only exists inside methods. Outside the class, use the object's own name: milk.title, not self.title.
The output shows
Parentheses are missing. Task is the class and Task(...) makes an object. milk.finish is the method and
milk.finish() calls it.
NameError: name 'task' is not defined
The class is Task with a capital T. Python treats task and Task as different names.
Exercises¶
- Book. Write a
Bookclass with a title, an author and a page count, a__str__that givesDune by Frank Herbert, 412 pages, and a methodis_long()that returnsTruefor more than 300 pages. Make a list of three books and print each, with a note after the long ones. - Bank account. Write a
BankAccountclass with an owner and a balance that starts at 0, methodsdeposit()andwithdraw(), and a__str__.withdraw()must refuse, with a message, to take out more than the balance. - Dice. Write a
Dieclass that takes a number of sides, with aroll()method that returns a random result and also counts how many times the die has been rolled. Make a six-sided and a twenty-sided die and roll each five times. - Tasks to lines. Write
task_to_line(task)andtask_from_line(line)forTaskobjects, using the Module 9 file format.task_from_line()should return a newTask, finished if the line saysyes. Make a done task, turn it into a line and back, and check that every attribute survived.
Solution 1
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
return f"{self.title} by {self.author}, {self.pages} pages"
def is_long(self):
"""Return True if the book has more than 300 pages."""
return self.pages > 300
books = [
Book("The Hobbit", "J. R. R. Tolkien", 310),
Book("Animal Farm", "George Orwell", 112),
Book("Dune", "Frank Herbert", 412),
]
for book in books:
print(book)
if book.is_long():
print(" That is a long one.")
is_long() returns a boolean, so it reads naturally in an if. A method that answers a yes-or-no question is
usually named is_something, as is_overdue() was.
Solution 2
class BankAccount:
def __init__(self, owner):
self.owner = owner
self.balance = 0
def __str__(self):
return f"{self.owner}'s account: {self.balance}"
def deposit(self, amount):
"""Add money to the account."""
self.balance += amount
def withdraw(self, amount):
"""Take money out, unless there is not enough. Return True if it worked."""
if amount > self.balance:
print(f"Cannot withdraw {amount}: only {self.balance} available.")
return False
self.balance -= amount
return True
account = BankAccount("Ada")
account.deposit(100)
account.withdraw(30)
account.withdraw(500)
print(account)
The balance is not a parameter, because a new account always starts at zero, just as a new task always starts
not done. withdraw() returns True or False so that a caller can find out whether it worked.
Solution 3
import random
class Die:
def __init__(self, sides):
self.sides = sides
self.rolls = 0
def roll(self):
"""Return a random result and count the roll."""
self.rolls += 1
return random.randint(1, self.sides)
six = Die(6)
twenty = Die(20)
for i in range(5):
print(f"d6: {six.roll()} d20: {twenty.roll()}")
print(f"The six-sided die was rolled {six.rolls} times.")
Each die keeps its own count in self.rolls, so rolling the twenty-sided die does not change the six-sided one's
number. That is the point of objects: each one has its own attributes.
Solution 4
from datetime import date
from task_methods import Task
def task_to_line(task):
"""Return a task as one line of text: title|YYYY-MM-DD|priority|yes or no."""
if task.done:
done = "yes"
else:
done = "no"
return f"{task.title}|{task.due}|{task.priority}|{done}"
def task_from_line(line):
"""Return the Task described by one line of the file. A malformed line causes a ValueError."""
title, due_text, priority_text, done_text = line.split("|")
task = Task(title, date.fromisoformat(due_text), int(priority_text))
if done_text == "yes":
task.finish()
return task
original = Task("Book flights, hotel and car", date(2026, 11, 1), 1)
original.finish()
line = task_to_line(original)
print(line)
back = task_from_line(line)
print(back)
print(back.title == original.title and back.due == original.due
and back.priority == original.priority and back.done == original.done)
task_from_line() is a plain function, not a method, because there is no task yet when it starts: making one is
its job. Save this next to task_methods.py, or the import fails. The next lesson puts these two functions to work
in the app.
Summary¶
- A
classdefines a new type. Calling the class,Task(...), makes an object of that type, exactly asdate(...)does. __init__(self, ...)runs when an object is made and creates its attributes withself.name = value.selfis the object being worked on. Every method takes it as its first parameter, and Python fills it in from the object before the dot.__str__(self)returns the textprint()shows for the object.- A method is a function inside a class.
task.describe_due(today)calls it on that task. - Objects live in lists, get passed to functions and are sorted with
key=, like any other value.