Week 2 · Python for real work and SQL
Classes and objects
Create your own types with classes, give them data and behaviour, and see what is shared between objects and what isn't.
A class is a blueprint. An object is a real thing made from that blueprint.
Think of a Project blueprint. It says that every project has a name. "Payments API" and "Website Redesign" are two objects made from it: same blueprint, different data.
How to follow along
Work in your learn-python project. Create a file called classes.py, type each example into it and run it with:
uv run classes.py
Type the code rather than pasting it, and run it after every small change. Before each run, ask yourself what it will print, then compare with the Output under the example. Clear the file before you type the next example.
The smallest useful class
class Project:
def __init__(self, name):
self.name = name
p1 = Project("Payments API")
p2 = Project("Website Redesign")
print(p1.name)
print(p2.name)
Output:
Payments API
Website Redesign
class Project:defines the blueprint.__init__runs automatically when you create an object.selfmeans "this particular object".self.name = namestores data on that object.
Project("Payments API") creates the object. Python passes the new object in as self for you, so you only supply name.
Adding a method
A function defined inside a class is called a method. You've been calling methods since week 1: .upper() is a method of strings, and .append() is a method of lists. Now you can write your own:
class Project:
def __init__(self, name):
self.name = name
self.archived = False
def archive(self):
self.archived = True
p1 = Project("Payments API")
p1.archive()
print(p1.archived)
Output:
True
Attributes are what an object has: name and archived. Methods are what an object does: archive().
Forgetting self
Everyone forgets self at some point, so make the mistake on purpose and see what it looks like. Remove it from the method's parameters:
class Project:
def __init__(self, name):
self.name = name
self.archived = False
def archive():
self.archived = True
p1 = Project("Payments API")
p1.archive()
Output:
TypeError: Project.archive() takes 0 positional arguments but 1 was given
You didn't pass anything to p1.archive(), yet the error says 1 argument was given. That argument is the object itself, which Python passes in as self.
Now put the parameter back, but leave out self. on the line inside the method:
class Project:
def __init__(self, name):
self.name = name
self.archived = False
def archive(self):
archived = True
p1 = Project("Payments API")
p1.archive()
print(p1.archived)
Output:
False
This one is worse, because there's no error. archived = True creates an ordinary variable that disappears when the method ends, so the object never changes. To store something on the object, you must write self..
Objects are independent
Each object keeps its own data. Create two projects, archive only one of them and print both:
class Project:
def __init__(self, name):
self.name = name
self.archived = False
def archive(self):
self.archived = True
p1 = Project("Payments API")
p2 = Project("Website Redesign")
p1.archive()
print(p1.name, p1.archived)
print(p2.name, p2.archived)
Output:
Payments API True
Website Redesign False
Both projects came from the same blueprint, and the same archive() method exists on both. Inside the call p1.archive(), self is p1, so only p1 changes.
Class attributes and instance attributes
An object made from a class is also called an instance of it. There are two places to keep an attribute:
| Where you write it | Who it belongs to | |
|---|---|---|
| Instance attribute | Inside a method, on self: self.name = name | One object. Each has its own value. |
| Class attribute | Directly inside the class: default_separator = "-" | The class. Every object shares one value. |
This class uses both, to turn a project's name into a short code that's safe to use in a URL or folder name:
class Project:
default_separator = "-"
def __init__(self, name):
self.name = name
def slug(self):
return self.name.lower().replace(" ", self.default_separator)
p1 = Project("Payments API")
p2 = Project("Website Redesign")
print(p1.slug())
print(p2.slug())
Project.default_separator = "_"
print(p1.slug())
print(p2.slug())
Output:
payments-api
website-redesign
payments_api
website_redesign
Each project has its own name, but there's only one default_separator. Changing it on the class changed the result for both projects.
Why this matters for an FDE
Real programs handle things with both data and rules: a project that can be archived, an order that can be closed, a ticket that can be escalated. A class keeps the data and the rules for changing it in one place, so the code that uses it can call project.archive() without knowing how archiving works.
You've already used one class of your own: class TaskStatus(StrEnum): in Enums and the standard library. You'll also see classes all through the libraries you use in later weeks, so being able to read one matters as much as being able to write one.