Skip to content

Classes & Object-oriented programming (OOP)

Object-oriented programming groups related data and the functions that act on it into a single unit, instead of keeping them separate. A dictionary can already hold a snake's data as key-value pairs — a class goes one step further, bundling that data together with the behavior (methods) that belongs to it.

classDiagram
    class Snake {
        +species
        +length_ft
        +__init__(species, length_ft)
        +describe()
        +__str__()
    }
    class Boa {
        +region
        +__init__(species, length_ft, region)
        +describe()
        +habitat()
    }
    Snake <|-- Boa

Fig. 9a — example of a class

Concept Example What it is
Class class Snake: The blueprint — defines what data and behavior every object built from it will have
Object (instance) ball = Snake("ball", 5) One specific thing built from the blueprint, with its own independent copy of the data
Attribute self.species A piece of data that belongs to an object
Method def describe(self): A function that belongs to a class and acts on a specific object
Inheritance class Boa(Snake): A new class that reuses — and can extend or override — another class's attributes and methods

Classes and objects

A class is a blueprint for creating objects — it defines what attributes and methods every object built from it will have. An object is one specific instance built from that blueprint, with its own copy of the attributes.

class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

ball = Snake("ball", 5)

print(ball.species)
print(ball.length_ft)

The __init__() method

Runs automatically every time a new object is created. It's where you set up the object's starting attributes. Python calls this a constructor.

ball = Snake("ball", 5)    # __init__ runs automatically, setting ball.species and ball.length_ft
Avoid mutable default arguments

A default argument's value is created once, when the method is defined — not fresh for every object. For a mutable default like a list or dict, every object that doesn't pass its own value ends up sharing that exact same one.

class Snake:
    def __init__(self, species, tags=[]):    # tags=[] is created once, not per-object
        self.species = species
        self.tags = tags

ball = Snake("ball")
ball.tags.append("captive-bred")

burmese = Snake("burmese")
print(burmese.tags)    # ["captive-bred"] — leaked from ball, since both share the same list

Use None as the default instead, and build a fresh list inside __init__ only if nothing was passed:

class Snake:
    def __init__(self, species, tags=None):
        self.species = species
        self.tags = tags if tags is not None else []    # a new list every time

The self parameter

Refers to the specific object a method was called on. self is the first parameter of every method in a class — it's how ball.species and burmese.species hold different values while sharing the same class. Python passes it in automatically; you never supply it yourself when calling a method (ball.describe(), not ball.describe(ball)).

self.species    # inside a method, refers to *this* object's own species — "ball" for ball, "burmese" for burmese

Object methods

A method is just a function defined inside a class. Since it always receives self, it can read (or change) that specific object's own attributes.

ball.describe()    # "a 5 ft ball python"

Going further

The __str__() method

Controls what print() shows for an object, instead of its memory address. By default, print()-ing an object just shows its memory address, which isn't very useful.

print(ball)    # without __str__: <__main__.Snake object at 0x...>
               # with __str__:    "ball python, 5 ft"
The __repr__() method

Controls what repr() returns for an object — used when Python needs a representation and there's no __str__() to fall back on, like printing an object inside a list.

print([ball])    # without __repr__: [<__main__.Snake object at 0x...>]
                  # with __repr__:    [Snake('ball', 5)]

Convention is to make it look like the code that would recreate the object — unlike __str__()'s more casual, human-readable description.

def __repr__(self):
    return f"Snake({self.species!r}, {self.length_ft})"
Modify & delete attributes

Assign to object.attribute to change it after creation. del object.attribute removes a single attribute; del object removes the object itself.

ball.length_ft = 6      # change an attribute directly, like any variable
del ball.length_ft      # remove just that attribute
del ball                # remove the whole object
pass placeholder

A placeholder for a class you haven't filled in yet. Same as in a loop or function — an empty class body is a syntax error on its own.

class Snake:
    pass    # an empty class body — valid syntax, nothing defined yet
Run a classes and objects example

All the examples above, combined into one script:

class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

ball = Snake("ball", 5)

print(ball.species)
print(ball.length_ft)

burmese = Snake("burmese", 16)
print(burmese.species)
print(burmese.length_ft)


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

ball = Snake("ball", 5)
burmese = Snake("burmese", 16)

print(ball.species)
print(burmese.species)


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def describe(self):
        return f"a {self.length_ft} ft {self.species} python"

ball = Snake("ball", 5)
print(ball.describe())


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def __str__(self):
        return f"{self.species} python, {self.length_ft} ft"

ball = Snake("ball", 5)
print(ball)


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

ball = Snake("ball", 5)
ball.length_ft = 6
print(ball.length_ft)

del ball.length_ft
print(ball.species)

del ball
print("ball object deleted")


class Snake:
    pass

s = Snake()
print(s)

Inheritance

A child class reuses — and can extend or override — everything defined in a parent class, instead of rewriting it from scratch. The parent is also called the base class; the child is the derived class.

class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def describe(self):
        return f"a {self.length_ft} ft {self.species} python"


class Boa(Snake):
    pass

boa = Boa("boa constrictor", 10)
print(boa.describe())

Overriding __init__()

Adding __init__() to a child class replaces the parent's version entirely. Call Parent.__init__(self, ...) explicitly inside it if you still want the parent's setup to run too.

class Boa(Snake):
    def __init__(self, species, length_ft, region):
        Snake.__init__(self, species, length_ft)
        self.region = region

Using super()

Calls the parent's version of a method without naming the parent class directly. The usual, cleaner way to do what the previous example did by hand.

super().__init__(species, length_ft)    # same as Snake.__init__(self, species, length_ft), without naming the parent

Adding attributes and methods

A child class isn't limited to what its parent has. It can define brand-new attributes and methods of its own, on top of everything it inherits.

boa.region       # "south america" — new attribute, parent Snake has no such thing
boa.habitat()    # new method, only Boa has it

Overriding methods

Defining a method in the child class with the exact same name as one in the parent replaces the parent's version for that child. This is the foundation of polymorphism, covered next.

snake.describe()    # "a 5 ft ball python"        — Snake's own version
boa.describe()      # "a heavy-bodied constrictor" — Boa's version replaces it

Going further

Run an inheritance example

All the examples above, combined into one script:

class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def describe(self):
        return f"a {self.length_ft} ft {self.species} python"


class Boa(Snake):
    pass

boa = Boa("boa constrictor", 10)
print(boa.describe())


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft


class Boa(Snake):
    def __init__(self, species, length_ft, region):
        Snake.__init__(self, species, length_ft)
        self.region = region

boa = Boa("boa constrictor", 10, "south america")
print(boa.species)
print(boa.region)


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft


class Boa(Snake):
    def __init__(self, species, length_ft, region):
        super().__init__(species, length_ft)
        self.region = region

boa = Boa("boa constrictor", 10, "south america")
print(boa.species)
print(boa.region)


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft


class Boa(Snake):
    def __init__(self, species, length_ft, region):
        super().__init__(species, length_ft)
        self.region = region

    def habitat(self):
        return f"found in {self.region}"

boa = Boa("boa constrictor", 10, "south america")
print(boa.region)
print(boa.habitat())


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def describe(self):
        return f"a {self.length_ft} ft {self.species} python"


class Boa(Snake):
    def describe(self):
        return "a heavy-bodied constrictor"

snake = Snake("ball", 5)
boa = Boa("boa constrictor", 10)

print(snake.describe())
print(boa.describe())

Polymorphism

Polymorphism ("many forms") means the same method or function name behaves differently depending on which object it's called on — so you can call .describe() on any snake-like object without needing to know exactly which one it is.

print(len("burmese python"))
print(len(["ball", "burmese", "boa"]))
print(len({"species": "ball", "length_ft": 5}))

Same method name, unrelated classes

Classes don't need to be related by inheritance to share a method name. As long as each one defines its own .move(), calling it works the same way no matter which object it's called on.

ball.move()     # "slither"
gecko.move()    # "climb"

Polymorphism via inheritance

Looping over a mix of parent and child objects and calling the same method name runs each object's own version automatically. This is the more common case — a child class overrides a parent's method, as in the previous section.

for s in (snake, boa): print(s.describe())
# a 5 ft ball python
# a heavy-bodied constrictor

Going further

Run a polymorphism example

All the examples above, combined into one script:

print(len("burmese python"))
print(len(["ball", "burmese", "boa"]))
print(len({"species": "ball", "length_ft": 5}))


class Snake:
    def move(self):
        print("slither")


class Gecko:
    def move(self):
        print("climb")

ball = Snake()
gecko = Gecko()

for animal in (ball, gecko):
    animal.move()


class Snake:
    def __init__(self, species, length_ft):
        self.species = species
        self.length_ft = length_ft

    def describe(self):
        return f"a {self.length_ft} ft {self.species} python"


class Boa(Snake):
    def describe(self):
        return "a heavy-bodied constrictor"

snake = Snake("ball", 5)
boa = Boa("boa constrictor", 10)

for s in (snake, boa):
    print(s.describe())