Object-oriented programming is one of Python's most powerful features, but also one that requires a large conceptual leap for beginners. The benefits of OOP are that the components of your program -- the data, and functions -- can be structured into "objects" that interact with each other, in an analogy to the way that the real world is also made of interacting objects. This provides an intuitive way to organize large programming projects and complex data structures. Moreover, OOP is at the core of how the Python language is structured. Even if you don't end up making your own objects and classes very often in your code, your understanding of how Python really works, as well as how most of the imported modules you are used to using really work, will be vastly expanded once these concepts are understood.
We will gradually build the intuition behind OOP before getting into the nitty-gritty -- jumping straight into examples will lead to confusion.
The good news is you have already met examples of objects. In fact, in Python, everything is an object -- every string, list, and function is an object. For example,
"Hello world"
'Hello world'
is an object. It contains some data -- most importantly, the text -- and some functions. What kind of functions? Well, for instance
"Hello world".split()
['Hello', 'world']
The function split()
is a function that in some sense belongs to the string. This is the meaning of the .
operator -- it is like the / in a file path. For example, Documents/my-cv.pdf means that "my-cv.pdf" somehow belongs to the folder "Documents". In Python, "Hello world".split()
means that split()
somehow belongs to the object "Hello world"
. This concept is actually very familiar. We know that to get the square root function, we must import the math
module, and then write math.sqrt(x)
to use the function. The . operator shows that the sqrt()
functions belongs to the module math
.
Moreover, "Hello world"
has some other behaviours built into it. For instance, if I write
"Hello world" + "! Good morning, morning!"
'Hello world! Good morning, morning!'
we see that this object knows what to do if it meets the + operator. So this humble little piece of text is actually packing a whole lot of complex behaviours! You will likewise find the same pattern in every Python data type you have met so far. A list, for instance, contains the entries in the list as its data; it knows some functions such as list.append()
; and it even knows what to do in situations such as
[1, 2] + [3, 4]
[1, 2, 3, 4]
Until now, you have probably thought of "string" or "list" as being a "type". This is, not wrong, of course. But we need a new piece of terminology to move in to object-oriented thinking. "String" should now be thought of as a class. A class is the abstract blueprint for an object. The class 'str'
defines everything that a string can do. What data can a string contain? What functions does it know? How should it interact with other objects? What happens if you write "A string"[4]
? The abstract class of strings contains the blueprints for every string we create.
The deal with object-oriented programming is that we get to design our own classes, containing exactly the kind of data we want, with exactly the kind of behaviours we want. Then, our program can create objects using our class definition, called instances of the class, just like we have this general class of strings, and then create instances of strings every time we write "A string"
.
A vivid example might be the bad guys in a computer game. If the game is written using object-oriented programming, there will be in the code some class defining what is the bad guy -- this will be data such as what he looks like on the screen, his health, any weapons he is carrying (which will be objects in themselves!), and so on. Then he will have functions controlling his AI, what happens when his health reaches 0, and such. Then, the game will create multiple instances of this baddie to chase after the player, all of which contain their own collection of this rich information and behaviour.
The rich complexity of many objects from modules you might have already met in Sam Ball's Python, Data, and You tutorials such as numpy
's arrays and pandas
' frames are due to those modules containing well-designed array and data frame classes.
A final word of motivation. The concepts of object-oriented programming are significantly more abstract than anything up to this point. Do not be discouraged if it takes a while to 'click'.
From now on, we will use the terms attributes and methods to refer to data and functions stored inside objects and classes. You can see what attributes and methods any object has by using dir()
:
dir("Hello, world!")
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
Some of these methods will be familiar, others look strange and have underscores everywhere. We'll get onto those soon enough.
Before we look a more substantial example, let's look at the basic innards of a class, and how to make objects from it. To create a class, we use the python keyword class
followed by the class's name, traditionally starting with a capital letter. Straight away, we shall also define a method inside the class called __init__()
. Defining a method is exactly like defining a function.
class Beekeeper():
def __init__(self):
pass
What's going on here? Almost every class comes with a method called __init__()
. The underscores mean that this is a "magic" method (no, I'm not being patronizing, that's really what Python programmers call them). A magic method is a method that the Python interpreter automatically looks for in certain situations (more about this in 10.2)
In this case, Python knows to call this method whenever an instance of the class is created -- that's the magic part. You'll also notice that it takes a parameter which we have called self
. Every object method takes the object itself as its first parameter, and it is traditional to call it "self". The job of the __init__()
method is to organize the data in the object. Let's flesh this out now and see how it works.
class Beekeeper():
def __init__(self, name, age, field):
self.name = name
self.age = age
self.subject = field
Just like any other function, the arguments passed to the __init__()
method are forgotten once the function has finished running. Therefore, the __init__()
method in this example is being used to store the data provided by the arguments as attributes of the object. To create or change an attribute is just like creating a variable, except we specify which object the attribute belongs to using the dot operator. In this case, the attribute will be part of the object the method belongs to, so we use self
.
You can also call other class methods from inside __init__()
. For example:
class Beekeeper():
def __init__(self, name, age, field):
self.name = name
self.age = age
self.subject = field
self.confirm_creation()
def confirm_creation(self):
print("Beekeeper {} has been created!".format(self.name))
def read_details(self):
print("Hi, I'm {}, aged {} and studying {}!".format(self.name, self.age, self.subject))
Notice, we had to be precise inside the __init__()
method, and specify when calling the method that this method belonged to the object which we call self
. This is a general habit -- whenever want to refer to attributes or methods of an object, we must use the dot operator. This applies whether we are referring to that data from outside the object, or from inside, in which case we use self.
. Let's see now:
beekeeper1 = Beekeeper("Sam", 24, "mathematics")
Beekeeper Sam has been created!
This is how we create instances of the class Beekeeper
. The arguments passed when creating the instance are the arguments passed to the __init__()
method. Read the code and make sure this is clear before moving on.
Now we can access and change the data just as we might expect:
beekeeper1.age / 3
8.0
beekeeper1.subject = 'pure mathematics'
beekeeper1.read_details()
Hi, I'm Sam, aged 24 and studying pure mathematics!
beekeeper2 = Beekeeper("Rob", 33, 'energy solutions')
Beekeeper Rob has been created!
If you want your program to generate lots of instances of a class with a convenient way to access them, it can be helpful to use a dictionary.
# this is a list, not a dictionary! just for this contrived example!
keeper_info = [("Sam C", 24, "mathematics"),
("Sam B", 22, "mathematics"),
("Rob", 30, 'energy solutions')]
# this part is the dictionary!
keepers = {}
for keeper in keeper_info:
keepers[keeper[0]] = Beekeeper(keeper[0], keeper[1], keeper[2])
Beekeeper Sam C has been created! Beekeeper Sam B has been created! Beekeeper Rob has been created!
keepers['Sam B'].age + keepers['Sam C'].age
46
To learn the basic concepts of object-oriented programming, we will make a command-line to-do list app. The main objects will be lists and tasks.
This program should be made in a Python module (.py file), not in Jupyter notebook or an interactive Python session (this is due to the nature of the project and not because these interfaces do not support object-oriented programming -- they do!). It is recommended that you type the sample code into your module by hand rather than copying and pasting as this will force you to pay attention.
Firstly, copy and paste this code into a module, and read the comments. The structure of the program is that we have a dictionary containing objects belonging to a class called TodoList
, which we shall define soon. Each instance of TodoList
contains a list of tasks, each task itself being an object of the class Task
. The methods of TodoList
and Task
handle operations like creating new tasks, marking tasks as complete, moving tasks to a different list, etc.
def create_new_list(*args):
pass
def show_lists(*args):
pass
def create_new_task(option):
pass
def change_current_list(option):
pass
def clear_all(*args):
pass
def show(*args):
pass
def show_all(option):
pass
def move(option):
pass
def mark(option):
pass
def help_me(*args):
'''Displays Help for the application.
'''
print("""
newlist -- create a new list
newtask -- create a new task in the current list
show -- show the tasks in the current list
all -- show all tasks
goto <listname> -- change the current list
mark #number -- mark task number # of the current list as completed/not completed
move #number -- move task number # of the current list to a different list
clear -- remove tasks marked completed
exit -- get me out of here!
""")
# dictionary containing todo lists
# lists = {'Default': TodoList('Default')}
# there is a currently selected list on which operations like
# "show" and "newtask" work.
current_list = lists['Default']
print("Welcome. Please type an option or type ? to see list of commands")
# main loop of the program:
# get input, take the first word of the input as the command
while True:
user_input = input("> ")
command = user_input.split()[0]
commands = {'newlist': create_new_list,
'newtask': create_new_task,
'show': show,
'goto': change_current_list,
'lists': show_lists,
'mark': mark,
'move': move,
'?': help_me,
'clear': clear_all,
'all': show_all}
# look for the right function in the dictionary and call it
if command.lower() == 'exit':
print("See you!")
break
else:
try:
commands[command](user_input)
except KeyError:
"Command not recognized. Type ? to see a list of commands."
So, this is the bare bones of the program. At the moment, it takes user input... and does precisely nothing with it. So, our project will be to fill out the functions, and define the classes as we go. The first class we define will be todo lists -- the idea is that the user can have multiple todo lasts, say, one for work, one for personal use. The todolist will have a name, a list of tasks, and some basic behaviours. At the top of your module, we will define the class TodoList
:
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
Let's remind ourselves what this means. This is the class definition, and then the all important __init__()
method, which is run by each newly created instance of the class. In this case, it assigns the input argument name
to the attribute name
, and then creates a new attribute called tasks
, which is an empty list.
Now let's write the code to create the TodoLists
. Find the function called create_new_list()
, and add some code:
def create_new_list(option):
list_name= input("Please enter a name for your list: ")
lists[list_name] = TodoList(list_name)
Also, for this to work, we have to un-comment the line that says lists = {'Default': TodoList('Default')}
. This just creates the dictionary called lists
, and adds a so-called "Default" TodoList to get the ball rolling. So, this function creates a new TodoList, with a name specified by the user, and stores it in a dictionary with the name also acting as the key.
Let's also uncomment the line saying current_list = lists['Default']
. Now we have a variable for the todo list the user is currently viewing and editing. We should give the user the ability to change this list. So in the function change_current_list
, add the following code:
def change_current_list(option):
global current_list
# the user accesses this function with "goto <list name>"
# so we remove the "goto " part at the start to get just the name
list_name = option.lstrip('goto ')
try:
current_list = lists[list_name]
except KeyError:
print("Don't recognize list {}".format(list_name))
If you are unfamiliar with raising exceptions, this last bit of the code just tells Python what to do if a certain kind of error occurs instead of terminating the program -- in this case, what to do if the dictionary key does not exist. You can also create your own kinds of exceptions (hint: they are classes).
The user should also be able to see all the lists available to choose from. So let's fill out the function:
def show_lists(*args):
for item in lists:
print(item)
Now we want the user to be able to add new tasks to the currently selected list. First of all, we need to define what a task is! So let's write a new class called Task
, underneath the class TodoList
, which knows 4 things -- the task itself, the date the task should be done, which list the task belongs to, and whether the task has been completed yet. The first three we should define as parameters of the __init__()
method.
class Task():
'''Basic Task class. takes a description of the task, when to do it,
and a TodoList object it belongs to as initial arguments.
'''
def __init__(self, task, date, todolist):
self.task = task
self.date = date
self.todolist = todolist
self.completed = False
This next step will be slightly abstract, so take a moment to try to follow what is going on. We are going to make the action of adding new tasks to a list a method of the list. See if you can spot the interesting part in the following code which we add the the TodoList class:
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def add_task(self):
# any format is accepted as the date but a possible extension to the project
# would be to parse it into a date object and then write some code that
# alerts the user on the due date. see the datetime library.
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
self.tasks.append(Task(task, date, self))
The interesting part is the last line, of course. self.tasks.append()
is self explanatory. Then it is pretty clear that we are creating an instance of the class Task
. The interesting part is that last argument. Recall that the __init__()
method of Task
takes the TodoList
it belongs to as the last argument. So the TodoList
instance will pass itself as an argument to each new Task
it creates, by referring to itself as self
. We are not just passing the name of the list, but the whole thing, and all its data. Note that the following is equivalent:
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def add_task(spam):
# any format is accepted as the date but a possible extension to the project
# would be to parse it into a date object and then write some code that
# alerts the user on the due date. see the datetime library.
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
spam.tasks.append(Task(task, date, spam))
This is because methods always understand their first argument to be the instance itself, regardless of what we choose to call it. But the idiomatic name is self, and self is a sensible choice, and so we choose to conform to this style standard.
Now we just need to call this method whenever the user types newtask
, so just fill out this function:
def create_new_task(*args):
current_list.add_task()
The user will probably want to be able to view their lists, so let's add a method to the TodoList class that displays the tasks to the screen:
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def add_task(self):
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
self.tasks.append(Task(task, date, self))
def display(self):
for i, item in enumerate(self.tasks):
mark = "Not completed"
if item.completed:
mark = "Complete!"
print("{}. \t {} \t {} \t {}".format(i+1, item.task, mark, item.date))
enumerate()
is a fantastic built-in function that works like this:
for i, word in enumerate(['I', 'love', 'HiPy']):
print(i, word)
0 I 1 love 2 HiPy
It is much preferred over for i in range(len(a_list)):
in situations where you want both the list item and its index.
Now we just make sure the user can access the display()
method from our little command line. We'll give them two options to do this: show just the current list, or show all lists. Fill out the following functions like so.
def show(*args):
current_list.display()
def show_all(*args):
for todolist in lists.values():
print(todolist.name + ": ")
todolist.display()
We're almost there now. Let's give the user the ability to tick off when they have completed a task. Inside the Task
class, define the following method:
def mark_completed(self):
self.completed = not self.completed
And then fill in the mark()
function to call this method. We subtract 1 from the task number because we want the user to think of the tasks as being indexed by 1, 2, 3... while of course, Python thinks of them as indexed by 0, 1, 2...
This is another one where we want to take a number as an argument from the user, so we strip out the first part of the command 'mark '.
def mark(option):
try:
task_number = int(option.lstrip('mark ')) -1
current_list.tasks[task_number].mark_completed()
except KeyError:
print("Not a valid list item!")
Now that the tasks can be marked as completed, the user might want to be able to delete all of their completed tasks. This can be easily done with a list comprehension. Add this method to your TodoList
class.
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def display(self):
for i, item in enumerate(self.tasks):
mark = "Not completed"
if item.completed:
mark = "Complete!"
print("{}. \t {} \t {} \t {}".format(i+1, item.task, mark, item.date))
def add_task(self):
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
self.tasks.append(Task(task, date, self))
def clear_completed(self):
''' Rewrite task list, using only tasks with the completed attribute
set to False.
'''
self.tasks = [task for task in self.tasks if task.completed == False]
and then of course fill out this function:
def clear_all(*args):
for todos in lists.values():
todos.clear_completed()
The final option the user will have is to be able to move tasks from one list to another. This moving of objects from one structure into another is a simple but useful pattern for object-oriented programming in Python. In the Task
class, add this last bit of code.
class Task():
'''Basic Task class. takes a description of the task, when to do it,
and a TodoList object it belongs to as initial arguments.
'''
def __init__(self, task, date, todolist):
self.task = task
self.date = date
self.todolist = todolist
self.completed = False
def mark_completed(self):
self.completed = not self.completed
def move_task(self, new_list):
try:
lists[new_list].tasks.append(self)
self.todolist.tasks.remove(self)
self.todolist = new_list
except KeyError:
print("Not a valid list!")
return
There is a natural question here. Why is moving a task from one list to another a method of the Task
class? Why shouldn't the TodoList
classes handle passing the task between themselves? And the answer is: there's no reason why it shouldn't. In most cases, Python doesn't really care which class a method belongs to. We could also have made this an ordinary function. The point is that you, the programmer, get to decide how your code is organized, and what feels psychologically right to you. As I wrote this tutotrial, I felt that moving from one list to another is something that tasks do, rather than lists, so I made it a method of the task.
Let's finish this by writing the function that calls this method.
def move(option):
'''
Expects argument of the form "move #" where #
is the number of the task to be moved.
'''
try:
task_number = int(option.lstrip('move ')) - 1
except IndexError:
print("Not a valid list item!")
return
task = current_list.tasks[task_number].task
print("Move task \"{}\" to which list?".format(task) )
show_lists()
move_to = input("> ")
#why don't we need try-except here?
task.move_task(move_to)
We're basically done now. If you did everything right, you should have a module that looks something like this (you can also test it in the Notebook):
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def display(self):
for i, item in enumerate(self.tasks):
mark = "Not completed"
if item.completed:
mark = "Complete!"
print("{}. \t {} \t {} \t {}".format(i+1, item.task, mark, item.date))
def add_task(self):
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
self.tasks.append(Task(task, date, self))
def clear_completed(self):
''' Rewrite task list, using only tasks with the completed attribute
set to False.
'''
self.tasks = [task for task in self.tasks if task.completed == False]
class Task():
'''Basic Task class. takes a description of the task, when to do it,
and a TodoList object it belongs to as initial arguments.
'''
def __init__(self, task, date, todolist):
self.task = task
self.date = date
self.todolist = todolist
self.completed = False
def mark_completed(self):
self.completed = not self.completed
def move_task(self, new_list):
try:
lists[new_list].tasks.append(self)
self.todolist.tasks.remove(self)
self.todolist = new_list
except KeyError:
print("Not a valid list!")
return
lists = {'Default': TodoList('Default')}
def create_new_list(option):
list_name= input("Please enter a name for your list: ")
lists[list_name] = TodoList(list_name)
def show_lists(*args):
for item in lists.values():
print(item.name)
def create_new_task(option):
current_list.add_task()
def change_current_list(option):
global current_list
list_name = option.lstrip('goto ')
try:
current_list = lists[list_name]
except KeyError:
print("Don't recognize list {}".format(list_name))
def clear_all(*args):
for todos in lists.values():
todos.clear_completed()
def show(*args):
current_list.display()
def show_all(*args):
for todolist in lists.values():
print(todolist.name + ": ")
todolist.display()
def move(option):
'''
Expects argument of the form "move #" where #
is the number of the task to be moved.
'''
try:
task_number = int(option.lstrip('move ')) - 1
except IndexError:
print("Not a valid list item!")
return
task = current_list.tasks[task_number].task
print("Move task \"{}\" to which list?".format(task) )
show_lists()
move_to = input("> ")
#why don't we need try-except here?
task.move_task(move_to)
def mark(option):
try:
task_number = int(option.lstrip('mark ')) -1
current_list.tasks[task_number].mark_completed()
except KeyError:
print("Not a valid list item!")
def help_me(*args):
print("""
newlist -- create a new list
newtask -- create a new task in the current list
show -- show the tasks in the current list
all -- show all tasks
goto <listname> -- change the current list
mark #number -- mark task number # of the current list as completed/not completed
move #number -- move task number # of the current list to a different list
clear -- remove tasks marked completed
exit -- get me out of here!
""")
current_list = lists['Default']
print("Welcome. Please type an option or type ? to see list of commands")
# main loop of the program:
# get input, take the first word of the input as the command
while True:
user_input = input("> ")
command = user_input.split()[0]
commands = {'newlist': create_new_list,
'newtask': create_new_task,
'show': show,
'goto': change_current_list,
'lists': show_lists,
'mark': mark,
'move': move,
'?': help_me,
'clear': clear_all,
'all': show_all}
# look for the right function in the dictionary and call it
if command.lower() == 'exit':
saveload.save(lists)
print("See you!")
break
else:
try:
commands[command](user_input)
except KeyError:
"Command not recognized. Type ? to see a list of commands."
Welcome. Please type an option or type ? to see list of commands > newlist Please enter a name for your list: Sam's list > newtask What is your new task for list Default? Get dressed What date should you do the task? Today > show 1. Get dressed Not completed Today
Of course, to be actually useable as a todo-list, we need to be able to save and load the data. See the appendix for some code you can add to make this happen.
Quiz / Exercises
What is the first parameter of any method?
What is the purpose of the __init__()
method? How do I call it and pass arguments to it?
Rewrite the move()
function and move_task()
method so that it is a method of the TodoList
class, rather than Task
.
To refer to the current instance, it is traditional to use this
. True/False?
The Boolean value True
is an object. True/False (hint: does it have any methods or attributes?)
In part II of this three-part series, we will look at inheritance and composition. These dual techniques allow us to create new classes from classes we have already defined.
Appendix: Optional extras, technical stuff
Class attributes and instance attributes
What happens if we omit use of self
? Usually, we'll run into trouble, but it's worth looking at.
class NoSelf():
an_attribute = "Foo"
def __init__(self):
self.another_attribute = "Foo"
test1 = NoSelf()
test2 = NoSelf()
test1.another_attribute = "Bar"
test1.another_attribute
'Bar'
test2.another_attribute
'Foo'
NoSelf.another_attribute = "Bar"
test2.another_attribute
'Foo'
NoSelf.an_attribute = "Bar"
test2.an_attribute
'Bar'
What we have been doing so far by using self
is defining instance attributes. This means, the values they contain are local to the instance -- every instance of the class has its own unique version of the attribute. Omitting the self
part defines a class attribute. If this is modified at the class level, it is modified for all instances of the class.
Comparisons with other OOP languages
Many object-oriented programming languages such as Java have a concept of private and public methods and attributes. The idea is that a private method or attribute can only be accessed from within the object itself -- another method of that same object has to call it. In contrast, a public method can be called by any other part of the program . The idea is to provide some degree of safety and security -- preventing important variables from being changed willy-nilly, for instance. This is especially relevant in collaborative projects when allowing one person working on one part of the program to change important variables in another part of the program could have disasterous results.
In Python we do not have this concept -- the language is designed not to throw up these kinds of artificial barriers (whether this is a good thing is down to personal taste). However, Python programmers have their own solution -- if a method or attribute is supposed to be private, an underscore is added to the beginning such as _mymethod
. This doesn't actually do anything, it's just a warning to anyone else working with the code -- fiddle with this at your own risk!
Another common safety feature is the idea of "getters" and "setters". These are methods which control access to private attributes. For instance, I may have some attributes that I do not want to be changed except in a specific, safe way. Therefore, I make the attribute private, but then create a "setter" method, which provides a safe interface for modifying that attribute (for instance, checking that it is being changed to a sensible value). Again, while there's nothing stopping you from defining setters, Python doesn't really have the concept of private attributes. Therefore, setter methods are not usually necessary.
Adding a save/load feature
Feel free to use the functions defined below to create a save/load feature for your todo list. You will need to import the json
library, add the save/load functions, and make a couple of modifications new the main loop (these are highlighted with comments).
import json
class TodoList():
def __init__(self, name):
self.name = name
self.tasks = []
def display(self):
for i, item in enumerate(self.tasks):
mark = "Not completed"
if item.completed:
mark = "Complete!"
print("{}. \t {} \t {} \t {}".format(i+1, item.task, mark, item.date))
def add_task(self):
task = input("What is your new task for list {}? ".format(current_list.name))
date = input("What date should you do the task? ")
self.tasks.append(Task(task, date, self))
def clear_completed(self):
''' Rewrite task list, using only tasks with the completed attribute
set to False.
'''
self.tasks = [task for task in self.tasks if task.completed == False]
class Task():
'''Basic Task class. takes a description of the task, when to do it,
and a TodoList object it belongs to as initial arguments.
'''
def __init__(self, task, date, todolist):
self.task = task
self.date = date
self.todolist = todolist
self.completed = False
def mark_completed(self):
self.completed = not self.completed
def move_task(self, new_list):
try:
lists[new_list].tasks.append(self)
self.todolist.tasks.remove(self)
self.todolist = new_list
except KeyError:
print("Not a valid list!")
return
def load(lists):
try:
with open("todolists.json", "r") as f:
for line in f:
listdic = json.loads(line)
lists[listdic['name']] = TodoList(listdic['name'])
for task in listdic['tasks']:
lists[listdic['name']].tasks.append(Task(task[0], task[1], lists[listdic['name']]))
return 0
except:
return 1
def create_new_list(option):
list_name= input("Please enter a name for your list: ")
lists[list_name] = TodoList(list_name)
def show_lists(*args):
for item in lists.values():
print(item.name)
def create_new_task(option):
current_list.add_task()
def change_current_list(option):
global current_list
list_name = option.lstrip('goto ')
try:
current_list = lists[list_name]
except KeyError:
print("Don't recognize list {}".format(list_name))
def clear_all(*args):
for todos in lists.values():
todos.clear_completed()
def show(*args):
current_list.display()
def show_all(*args):
for todolist in lists.values():
print(todolist.name + ": ")
todolist.display()
def move(option):
'''
Expects argument of the form "move #" where #
is the number of the task to be moved.
'''
try:
task_number = int(option.lstrip('move ')) - 1
except IndexError:
print("Not a valid list item!")
return
task = current_list.tasks[task_number].task
print("Move task \"{}\" to which list?".format(task) )
show_lists()
move_to = input("> ")
#why don't we need try-except here?
task.move_task(move_to)
def mark(option):
try:
task_number = int(option.lstrip('mark ')) -1
current_list.tasks[task_number].mark_completed()
except KeyError:
print("Not a valid list item!")
def save(lists):
open("todolists.json", "w").close()
for todolist in lists.values():
listdic = {'name': todolist.name,
'tasks': [(item.task, item.date) for item in todolist.tasks]}
with open("todolists.json", "a") as f:
json.dump(listdic, f)
f.write('\n')
def help_me(*args):
print("""
newlist -- create a new list
newtask -- create a new task in the current list
show -- show the tasks in the current list
all -- show all tasks
goto <listname> -- change the current list
mark #number -- mark task number # of the current list as completed/not completed
move #number -- move task number # of the current list to a different list
clear -- remove tasks marked completed
exit -- get me out of here!
""")
### CHANGE THIS BIT
lists = {}
if load(lists):
lists = {'Default': TodoList('Default')}
current_list = lists['Default']
else:
current_list = lists[next(iter(lists))]
print("Welcome. Please type an option or type ? to see list of commands")
# main loop of the program:
# get input, take the first word of the input as the command
while True:
user_input = input("> ")
command = user_input.split()[0]
commands = {'newlist': create_new_list,
'newtask': create_new_task,
'show': show,
'goto': change_current_list,
'lists': show_lists,
'mark': mark,
'move': move,
'?': help_me,
'clear': clear_all,
'all': show_all}
# look for the right function in the dictionary and call it
if command.lower() == 'exit':
# AND CHANGE THIS BIT!!!!
save(lists)
print("See you!")
break
else:
try:
commands[command](user_input)
except KeyError:
"Command not recognized. Type ? to see a list of commands."