In From data to objects we defined a Task class and assigned attributes to it from outside, and we saw that this fixed nothing: we could still create a task without completed and with its priority written as "High". The only thing that changed was the name of the error, from KeyError to AttributeError. This lesson brings the missing piece, and it is the heart of the whole module. That piece is the constructor, __init__: a block that Python runs automatically when each instance is created, and which is therefore the only point in the program that every task ever to exist must pass through. That is where the essential data is demanded and its values validated. Alongside it come methods, which let us gather up the loose functions that have been wandering around easytask.py since module 4 and store them where they belong: inside the task itself. By the end of the lesson, EasyTask will be v0.15 and will work with objects.
Contents
__init__: the constructorself, properly explained- Validating inside the constructor
- Methods that read the state
- Methods that change the state
__str__versus__repr__- Comparing objects with
__eq__(and a note on__len__) - Class and method docstrings
@property: computed values without duplicated state@staticmethodand@classmethod@dataclass: the shortcut for classes that only hold data- EasyTask v0.15: the
Taskclass - Common mistakes and tips
- Exercises
- Conclusion
__init__: the constructor
__init__: the constructorThe constructor is a special method called __init__ (two underscores on each side) that Python runs immediately after creating an instance. Its job is to leave the object ready to use: to give a value to every one of its attributes.
class Task:
"""A task at Alba Studio."""
def __init__(self, title, assignee, priority="medium", days=1):
self.title = title
self.assignee = assignee
self.priority = priority
self.days = days
self.completed = False # every task is born pending
poster = Task("Book fair poster", "Luis", "high", 3)
print(poster.title, poster.priority, poster.completed) # ... high False
Task("Vidal quote")
# TypeError: __init__() missing 1 required positional argument: 'assignee'Several new things happen at once, and they are worth reading slowly:
__init__is never called by hand. You writeTask("Book fair...", "Luis", "high", 3)and Python creates the object and passes those arguments to__init__for you. It is the same mechanism by whichlist("abc")orint("42")build objects.- Parameters without a default value are the mandatory data.
titleandassigneehave none, so it is impossible to create a task without them: the error fires in the right place at the right moment, as the last line of the example shows. The ones that do have a default work exactly as in functions (04-02):priority="medium"anddays=1let you writeTask("Review sketches", "Nuria"), and they must come after the mandatory ones. completedis not a parameter: it is fixed inside. No task is born completed, so there is no sense in asking about it at creation time. Some attributes are received and some are fixed or computed, and all of them leave the constructor with a value. Before, the guarantee that a task had its five fields depended on every programmer remembering; now the guarantee belongs to the language: if the object exists, its attributes exist, and theAttributeErrorhalfway through the listing is no longer possible.
self, properly explained
self, properly explainedself is the word that confuses people most at first, and there is really no mystery to it: self is the object being worked on. When you write Task("Book fair poster", "Luis"), Python does two things: it manufactures an empty object and calls __init__ passing that object as the first argument. Inside the method, that first parameter is called self by convention, and self.title = title means "store the received value in this object".
| What you write | What Python actually runs |
|---|---|
poster = Task("Poster", "Luis") |
creates the object and calls Task.__init__(object, "Poster", "Luis") |
poster.complete() |
calls Task.complete(poster) |
poster.reassign("Nuria") |
calls Task.reassign(poster, "Nuria") |
Two rules come out of that and must be memorised: self is always the first parameter of every instance method, even if it receives nothing else; and self is not passed when calling, Python supplies it from the object on the left of the dot. That is why poster.complete() is written without arguments even though its definition is def complete(self). Two classic mistakes, with their exact message so you recognise them:
class Task:
def __init__(title, assignee): # WRONG: self is missing
...
Task("Poster", "Luis")
# TypeError: __init__() takes 2 positional arguments but 3 were givenThe message looks absurd —"you gave 3 and it accepts 2"— until you remember that Python adds the object as the first argument. Without self, the numbers never add up. The second mistake produces no message at all, and that is why it is worse: writing title = title inside the constructor instead of self.title = title. That assigns to a local variable which dies when the method ends (it is module 4's scope in action); the object is left without a title and the failure will show up later and somewhere else. The rule is simple: anything that must outlive the call is stored in self.. Finally, self is not a keyword —you could call it me and it would work—, but do not do it: everybody writes self.
- Validating inside the constructor
Having all five attributes is not enough: their values must be correct too. And since the constructor is the compulsory step for every task, it is the natural place to check.
PRIORITIES = ("high", "medium", "low")
TEAM = ("marta", "luis", "nuria")
class Task:
def __init__(self, title, assignee, priority="medium", days=1): # v0.15
self.title = title.strip() # drop stray spaces
clean = assignee.strip().lower()
self.assignee = clean.capitalize() if clean in TEAM else "Marta"
priority = priority.strip().lower() # 'High' -> 'high'
self.priority = priority if priority in PRIORITIES else "medium"
self.days = days if 1 <= days <= 365 else 1
self.completed = False
t = Task(" Sole Bakery logo ", "NURIA", "High", 5)
print(f"[{t.title}] {t.assignee} {t.priority} {t.days}d")
# [Sole Bakery logo] Nuria high 5dLook at what just happened: "High", the value that in module 6 was going to blow up PRIORITY_ORDER, can no longer get into the program. The class normalises it at the single point every task passes through, and it does the same with the spaces in the title and with "NURIA". There are three possible strategies when faced with an invalid value:
| Strategy | What it does | When it suits |
|---|---|---|
| Normalise | Fixes what is fixable: "High" → "high", strips spaces |
Differences of format, not of content |
| Safe value | Replaces the unrecoverable with a default | Doubtful data that should not stop the program |
| Warn and reject | Prevents the object from being created and reports the failure | The right thing in a serious program |
For now we use the first two deliberately. The third —raising an error with raise and catching it with try/except— is the content of Debugging and error handling; when you study it you will come back to this class and replace the safe values with explicit errors, without changing the structure. In the meantime there is a danger to watch: a silent safe value hides the problem. If somebody writes Task("Poster", "Pete") and the task ends up assigned to Marta without a word, the error exists but nobody sees it; at the very least you should warn on screen with a print(f"Warning: '{assignee}' is not on the team; assigned to Marta.") in that branch, and that is what the final version in section 12 does.
- Methods that read the state
A method is a function defined inside the class, with self as its first parameter, which therefore reaches the object's attributes directly. There are two families of them, and telling them apart helps you design well: the ones that query the state without touching it and the ones that modify it. We start with the former, which gather up logic that was until now scattered across loose functions (the class also has an attribute self.done = 0, the days already invested):
def is_overdue(self):
"""Say whether more days have been spent than estimated."""
return not self.completed and self.done > self.days
def urgency(self):
"""Return the real urgency: 'none', 'critical' or the priority."""
if self.completed:
return "none"
return "critical" if self.is_overdue() else self.priority
poster = Task("Book fair poster", "Luis", "high", 3)
poster.done = 5 # 5 days spent out of 3 planned
print(poster.is_overdue(), poster.urgency()) # True criticalThree observations that hold for every method you write. They do not receive the task as a parameter: we used to write is_overdue(task) and now the task is self and arrives on its own, so poster.urgency() says who and what without ambiguity. A method can call another one on the same object, always through self, as urgency does with self.is_overdue(); without the self., Python would look for a global function of that name and not find it. And these methods do not print: they return a value, which is the separation between input/output and logic from 04-04 applied inside classes; the caller decides whether to print it, add it up or use it to filter.
- Methods that change the state
The other family modifies the object's attributes. This is where mark_completed and change_priority, which since module 4 have been receiving a dictionary and modifying it in place, finally find their home:
def complete(self):
"""Mark the task as finished. Return False if it already was."""
if self.completed:
return False
self.completed = True
self.done = max(self.done, self.days)
return True
def reassign(self, person):
"""Change the assignee if the person belongs to the team."""
clean = person.strip().lower()
if clean not in TEAM:
return False
self.assignee = clean.capitalize()
return True
def advance(self, days_worked):
"""Add days of work already invested in the task."""
self.done += max(0, days_worked)
poster = Task("Book fair poster", "Luis", "high", 3)
print(poster.reassign("nuria"), poster.reassign("Pete")) # True False
print(poster.complete(), poster.complete()) # True False: the second changes nothingTwo design decisions worth copying. They return True/False to signal whether the change happened, so the caller warns the user without the method printing anything: if not poster.reassign(name): print("That person is not on the team."). And the method protects the invariant: reassign does not accept an assignee outside TEAM, just as the constructor did. That is the general rule of encapsulation: if a piece of data has rules, every path that modifies it must respect them, and that is why it is changed through methods and not by writing poster.assignee = "Pete" from outside.
__str__ versus __repr__
__str__ versus __repr__You already saw that print(poster) shows something unreadable: <__main__.Task object at 0x7f8b1c0d5e50>. It is fixed with two special methods:
def __str__(self): # text for the end user
mark = "[X]" if self.completed else "[ ]"
return f"{mark} {self.title:<28}{self.assignee:<8}{self.priority:<6}{self.days}d"
def __repr__(self): # text for the programmer
return f"Task({self.title!r}, {self.assignee!r}, {self.priority!r}, {self.days})"
poster = Task("Book fair poster", "Luis", "high", 3)
print(poster) # [ ] Book fair poster Luis high 3d
print(repr(poster)) # Task('Book fair poster', 'Luis', 'high', 3)
print([poster]) # [Task('Book fair poster', 'Luis', 'high', 3)]__str__ |
__repr__ |
|
|---|---|---|
| Who it is for | The user of the program | The programmer |
| Used by | print(obj), str(obj), f-strings |
The console, repr(obj), objects inside lists |
| Goal | To read well | To be precise and unambiguous |
| If you define only one | print works, but lists stay ugly |
print uses it too, as a stand-in |
The last row is the practical advice: if you are only going to write one, write __repr__, because Python uses it as a substitute when __str__ is missing and it is also the one you see while debugging. The most common surprise is print([poster]): when printing a list, Python does not use the __str__ of its elements but their __repr__, so a list of tasks without __repr__ still shows memory addresses. The !r in the f-string, by the way, is the shortcut for applying repr() to a value: that is why the strings come out in quotes.
- Comparing objects with
__eq__ (and a note on __len__)
__eq__ (and a note on __len__)By default, two different objects are never equal, even if they hold the same data. It is consistent with what we saw in 05-01 about is and ==, but it is almost never what we want. By defining __eq__ we decide ourselves what it means for two tasks to be the same:
def __eq__(self, other):
"""Two tasks are the same if title and assignee match."""
if not isinstance(other, Task):
return NotImplemented # comparing with anything else is not our job
return (self.title.lower() == other.title.lower()
and self.assignee == other.assignee)
a = Task("Book fair poster", "Luis", "high", 3)
b = Task("book fair poster", "Luis", "low", 9)
print(a == b, a == "Poster") # True False (without __eq__, the first would be False)
print(a in [b]) # True: the 'in' operator uses ==The interesting part is the last line: by defining __eq__, operators and functions you already know start working with your objects. in, .count(), .index() and .remove() on lists use == internally, so they now locate tasks by content. Returning NotImplemented when the other object is not a Task is the polite way of saying "I do not know how to compare this"; Python then answers False. In the same family is __len__, which defines what len(obj) returns. On a single task it makes no sense —the length of what?—, but in the Agenda class of Collections of objects it will be obvious: len(agenda) should give the number of tasks. These methods with underscores are called special methods or dunder (from double underscore), and their point is that they connect your classes with the syntax of the language.
- Class and method docstrings
Just like functions, classes and their methods carry a docstring: a piece of text between triple quotes on the first line of the body. You have seen it in every example in this lesson —"""Mark the task as finished. Return False if it already was."""—, with one difference: a class docstring usually spans several lines, because as well as saying what the object represents it describes what it guarantees, as in """A task at the studio. Born pending, validated and with every field set.""".
They are consulted with help(Task) or Task.complete.__doc__, and the editor shows them as you type. The minimum rule: the class explains what it represents and what it guarantees; each method, what it does and what it returns. The full guide is in Documentation and comments.
@property: computed values without duplicated state
@property: computed values without duplicated stateImagine you want to know how many days of work a task has left. The temptation is to store it as one more attribute, self.remaining_days = days, and that is where the problem starts: every time somebody calls advance() or complete() you have to remember to recompute it, and the day you forget, the object will show a false figure. That is duplicated state, one of the most silent sources of error there is. The solution is not to store it but to compute it on demand. And so that it reads like an attribute rather than a method, we use the @property decorator:
@property
def remaining_days(self):
"""Days of work left; zero if the task is already completed."""
return 0 if self.completed else max(0, self.days - self.done)
poster = Task("Book fair poster", "Luis", "high", 3)
print(poster.remaining_days) # 3 <- no parentheses: it looks like an attribute
poster.advance(2)
print(poster.remaining_days) # 1 <- always consistent: recomputed
poster.complete()
print(poster.remaining_days) # 0 <- and still consistentThe difference from a stored attribute lies in consistency: both are read the same way, with obj.x and no parentheses, but the attribute has to be updated by hand in every method that affects it, whereas the @property is computed on the spot and therefore never lies. In exchange it pays a small computation per read, which outside enormous loops is irrelevant. Decorators will already ring a bell from @lru_cache (06-03): they are functions that wrap another one to modify its behaviour. The practical rule: if a value can be deduced from other attributes, make it a @property; if it is independent data, store it. And note that a read-only @property does not accept assignment —poster.remaining_days = 5 raises AttributeError—, which is a protection, not an inconvenience.
@staticmethod and @classmethod
@staticmethod and @classmethodNot every method needs a concrete object. A @staticmethod is an ordinary function that lives inside the class out of thematic affinity: it receives no self and touches no object. It is useful for domain utilities, such as checking a value before creating anything:
@staticmethod
def valid_priority(text):
"""Say whether a piece of text is an acceptable priority."""
return text.strip().lower() in PRIORITIES
print(Task.valid_priority("HIGH")) # True: from the class, with no instanceA @classmethod receives the class (cls) as its first parameter instead of the instance, and its most valuable use is the alternative constructor: another way of manufacturing objects from data in a different format. Exactly what we need for the JSON of 05-05, where each task is stored as a dictionary:
@classmethod
def from_dictionary(cls, data):
"""Create a Task from a dictionary read out of the JSON file."""
task = cls(data["title"], data["assignee"],
data.get("priority", "medium"), data.get("days", 1))
task.done = data.get("done", 0)
task.completed = data.get("completed", False)
return task
d = {"title": "Vidal quote", "assignee": "Marta", "priority": "High"}
print(Task.from_dictionary(d)) # [ ] Vidal quote Marta high 1dNotice the detail that makes it robust: data.get("priority", "medium") tolerates the key being missing and the constructor normalises the "High" that came from the file. In other words, the dirty dictionaries of module 6 go in, and clean tasks come out. The reverse operation —turning the object into a dictionary so it can be saved— is to_dict(), and it arrives in 07-03 alongside the Agenda class, because json.dump cannot write objects.
@dataclass: the shortcut for classes that only hold data
@dataclass: the shortcut for classes that only hold dataWhen a class does nothing but group data together, writing __init__, __repr__ and __eq__ by hand is pure paperwork. The standard library's dataclasses module generates them for you. The same Client, before and after:
class Client: # BEFORE: all by hand
def __init__(self, name, contact, active=True):
self.name, self.contact, self.active = name, contact, active
def __repr__(self): ... # the f-string with the three fields
def __eq__(self, other): ... # isinstance + field-by-field comparison
from dataclasses import dataclass # AFTER: the same, generated
@dataclass
class Client:
"""A client of the studio."""
name: str
contact: str
active: bool = True
sole = Client("Sole Bakery", "[email protected]")
print(sole, sole == Client("Sole Bakery", "[email protected]"))
# Client(name='Sole Bakery', contact='[email protected]', active=True) TrueThe decorator reads the name: str lines —type annotations, which Python does not check but which serve as documentation and help the editor— and generates __init__, __repr__ and __eq__. When to use which: if the class only groups data, @dataclass; if there is validation, normalisation, invariants or behaviour of its own, an ordinary class with a hand-written __init__. Our Task validates, normalises and decides, so it stays an ordinary class; Client, which only carries data, is a textbook @dataclass.
- EasyTask v0.15: the
Task class
Task classLet us apply the lot. The loose functions that worked on dictionaries become methods, and the agenda becomes a list of Task objects.
# easytask.py - Alba Studio / Version 0.15: the task is an object
FIELDS = ("title", "assignee", "priority", "days", "done", "completed")
# --- The rest of the constants and I/O functions: unchanged since v0.14 ---
class Task:
"""A task at the studio. Born pending, validated and with every field set."""
# __init__ as in section 3, plus the warning for an invalid assignee and
# self.done = 0; remaining_days (@property), is_overdue, urgency,
# complete, reassign, change_priority, advance, from_dictionary,
# __str__, __repr__ and __eq__ exactly as written in this lesson
def sort_key(self):
"""Listing criterion: priority, then days, then title."""
return (PRIORITY_ORDER[self.priority], self.days, self.title)
def register_task(agenda):
"""Ask for a new task and add it to the agenda."""
agenda.append(Task(ask_text("Title : "),
ask_option("Assignee : ", TEAM),
ask_option("Priority : ", PRIORITIES),
ask_integer("Days (1-365) : ", 1, 365)))
def show_list(agenda):
"""Show the agenda sorted by priority and days."""
for number, task in enumerate(sorted(agenda, key=Task.sort_key), start=1):
print(f"{number:>2}. {task}") # this is where __str__ actsTwo new details. In the print inside the loop, {task} invokes __str__ automatically: the function no longer knows anything about the internal structure of a task. And key=Task.sort_key works because a method reached from the class is an ordinary function that receives the object as its first argument, so sorted will pass each task as self; key=lambda t: t.sort_key() would work too. Before and after, line by line:
| In v0.14 (dictionaries) | In v0.15 (objects) |
|---|---|
task["title"] |
task.title |
show_card(task) |
print(task), thanks to __str__ |
sort_key(task) / mark_completed(task) |
task.sort_key() / task.complete() |
Validation spread through register_task |
Centralised in __init__ |
A possible KeyError in any listing |
Impossible: if the object exists, it has its fields |
One loose end is obvious: save_tasks no longer works. json.dump knows how to write dictionaries, but not Task objects, and it complains with TypeError: Object of type Task is not JSON serializable. The solution —a to_dict() method returning the equivalent dictionary, with from_dictionary for the way back— arrives in the next lesson.
Common Mistakes and Tips
- Forgetting
selfin the method definition, with the messagetakes 1 positional argument but 2 were given. Remember that Python adds the object: if the method receivesnarguments when called, its definition needsn + 1parameters. And forgetting theself.when assigning inside the constructor gives no error at all: it creates a local variable that is lost, so if an attribute "disappears", check this first. - Putting a mutable as a parameter's default value,
def __init__(self, notes=[]). It is the same mistake from module 4 and here it is worse, because that list is shared by every instance. Usenotes=Noneand insideself.notes = notes if notes else []. Nor is it ever necessary to call__init__by hand (poster.__init__(...)): writeTask(...). - Defining only
__str__and expecting lists to look good.print([task])uses__repr__: define both, or at least__repr__. And do not modify attributes from outside, bypassing the methods (task.priority = "High"), because it breaks exactly the guarantee the class provides: if a piece of data has rules, go through the method. - Tip: put the methods in order. First
__init__, then the@propertyblocks, then the methods that query, then the ones that modify, and the special ones last. A tidy class can be read at a glance.
Exercises
Exercise 1: The Client class
Write a Client class whose constructor receives name, contact and credit_days=30; which normalises the name (no stray spaces and with the first letter of each word capitalised), forces credit_days to be between 0 and 90 (otherwise 30) and sets invoices = []. Add an invoice(amount) method that appends the amount to the list if it is positive and returns True/False, and a @property total_invoiced. Try it out with Solé Bakery.
Exercise 2: A method from a loose function
This function worked with dictionaries in v0.14. Turn it into a summary() method of Task without changing its behaviour and explain in writing what improves: return f"{task['title']} ({task['assignee']}): {status}, {task['days']} days", where status is "completed" or "pending" depending on task["completed"].
Exercise 3: @property versus stored attribute
Add to Task a @property called percentage that returns progress as a percentage (days done over days estimated, capped at 100, and 100 if it is completed). Then answer in writing: what would happen if instead of a @property you stored self.percentage in the constructor? Write the sequence of calls that would demonstrate the failure.
Solutions
Solution 1.
class Client:
"""A client of the studio, with its credit and its invoices."""
def __init__(self, name, contact, credit_days=30):
self.name = name.strip().title()
self.contact = contact.strip().lower()
self.credit_days = credit_days if 0 <= credit_days <= 90 else 30
self.invoices = [] # mutable: always created in the constructor
@property
def total_invoiced(self):
"""Sum of every invoice issued to the client."""
return sum(self.invoices)
def invoice(self, amount):
"""Add an invoice if the amount is positive."""
if amount <= 0:
return False
self.invoices.append(amount)
return True
sole = Client(" sole bakery ", "[email protected]", 120)
sole.invoice(450.0)
print(sole.name, sole.credit_days, sole.invoice(-10), sole.total_invoiced)
# Sole Bakery 30 False 450.0 <- name normalised, safe credit, rejectionThe key is self.invoices = [] inside the constructor: if it were in the body of the class it would be a shared class attribute and one client's invoices would show up in the others (07-01, section 7). And total_invoiced is a @property because it is a sum deducible from invoices: storing it would force you to update it on every invoice.
Solution 2.
def summary(self):
"""Return a descriptive line for the task."""
status = "completed" if self.completed else "pending"
return f"{self.title} ({self.assignee}): {status}, {self.days} days"Three concrete improvements. It lives next to the data: if tomorrow the task gains a field, the summary is three lines away and not somewhere else in the file. It cannot receive something that is not a task: task_summary({"title": "x"}) blew up with KeyError, whereas task.summary() only exists if task is a Task. And it reads better at the point of use: print(poster.summary()) says whose summary it is without having to look at any function's signature.
Solution 3.
@property
def percentage(self): # progress, between 0 and 100
if self.completed:
return 100
return min(100, round(self.done / self.days * 100))
# If instead of the @property you stored self.percentage = 0 in the constructor:
t = Task("Book fair poster", "Luis", "high", 4)
t.advance(2) # half of it is done
print(t.percentage) # 0 <- A LIE: nobody updated the attributeadvance modifies done, but the percentage attribute keeps the value the constructor gave it. That is duplicated state: the same fact —how much progress has been made— stored in two places that can disagree. With the @property, the percentage does not exist until it is asked for and therefore cannot be out of date. The same trap would appear with complete(), which also changes done without touching percentage.
Conclusion
The constructor __init__ is the compulsory step for every object of a class, and that is why it is where we guarantee that every task is born complete: parameters without a default value are the essential data, those with one cover the optional part, and the attributes that are not received —such as completed— are fixed inside. That is also where we validate: normalising "High" to "high" and checking the assignee against TEAM eliminates module 6's KeyError at the root, and when you study raise and try/except in 08-02 you will replace the safe values with explicit errors. self is not magic: it is the object itself, which Python passes as the first argument based on what sits to the left of the dot, and anything that must outlive the call is stored in self.. Methods gather up the logic that was lying loose, distinguishing the ones that query the state (is_overdue, urgency, sort_key) from the ones that modify it (complete, reassign, change_priority), which return True/False instead of printing. Special methods connect the class with the language: __str__ for the user, __repr__ for the programmer —and it is the one lists use—, __eq__ so that == and in compare content. @property turns any value deducible from the others into a read-only attribute and removes duplicated state; @staticmethod houses object-free utilities and @classmethod provides alternative constructors such as Task.from_dictionary(d), the entry point for the JSON data. And @dataclass saves the manual work in classes that only group data.
EasyTask is now v0.15: the agenda is a list of Task objects, the listing is printed with {task} and seven loose functions have moved inside the class. But look at what has been left dangling. The agenda is still a bare list: any part of the program can append whatever it likes to it —including a stray dictionary or a number—, the operations that handle it (search, filter, sort, summarise) are still spread around the file, and save_tasks is literally broken because json.dump cannot write objects. In Collections of objects we will take today's same leap, but one level up: enter the Agenda class, which contains the tasks and offers add, find, filter and summary; you will learn to iterate, sort and index objects, you will see why a collection must protect its internal list, and you will recover persistence with to_dict/from_dict so we can keep saving to the same JSON file from 05-05.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
