Skip to content

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.

Live session

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

classes.py
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.
  • self means "this particular object".
  • self.name = name stores 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:

classes.py
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:

classes.py
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:

classes.py
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:

classes.py
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 itWho it belongs to
Instance attributeInside a method, on self: self.name = nameOne object. Each has its own value.
Class attributeDirectly 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:

classes.py
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.