⇦ Back

When you code in Python, you create and use objects which are each an instance of an object type. Consider the following code:

st = 'Hello, World'

Each object type has its own functionality: strings can be formatted and printed like text, integers can be used to do maths and Booleans can represent the output or result of a test. This means that an instance of an object type both contains data and has functionality.

Classes are essentially custom object types. Forget using strings, integers and Booleans: you can create your own types!

1 The Basics

Here’s how to create a very simple class (so simple that it has no functionality):

class ClassName:
    """A simple example class."""
    pass

In other words, you need to:

  • Use the class statement
  • Provide a name for the class (in this example we’ve used ClassName). The convention for class names is to use ‘CapWords’ - each word in the class’s name starts with a capital letter and there is no space or underscore between them.
  • The class name is followed by a colon (:)
  • The ‘contents’ of the class is indented away from the left-hand margin
  • Another convention is to have the first line of the class be a ‘docstring’:
    • A docstring is a string that documents (describes) what the class is
    • It is written as a full sentence - starting with a capital letter and ending with a full stop - but one which only contains an object (ie you could add the words “this is” to the start and it would make sense, for example “(This is) A simple example class.”)
    • The string is created using a pair of triple quotation marks
  • The statements inside the indented block of code make up the content of the class definition. In this example we just have the pass statement, which does nothing, hence our class has no functionality.

An instance of this class is created as follows:

# Notice the round brackets after the class name indicating that the class
# definition is being 'called'
instance = ClassName()

This instance contains no data and has no functionality, but it exists!

1.1 Attributes

Let’s imagine we want to create a database of Olympic athletes. We can create a class (object type) called “TrackAthlete” to contain data relevant to individual people:

class TrackAthlete:
    """A Python object representing a track athlete."""
    pass


# Create an instance of the TrackAthlete class
athlete1 = TrackAthlete()

Notice that there are two blank lines after the class definition. This is another convention: definitions should be preceded and followed by two blank lines.

So “athlete1” is an instance of a track athlete but, currently, it contains no data. We can give the athlete a name by creating and setting an attribute called “name”:

# Create an attribute
athlete1.name = 'Usain Bolt'
print(athlete1.name)
## Usain Bolt

An attribute is a piece of information stored inside an object. It is set and called using ‘dot notation’: object.attribute.

1.2 Member Variables

Member variables are attributes that are automatically created when an instance is created (and so all objects of a particular type have them). They are defined in the class definition:

class TrackAthlete:
    """A Python object representing a track athlete."""
    # Create a member variable
    sport = 'Track and Field'


# Create an instance of the TrackAthlete class
athlete1 = TrackAthlete()
print(athlete1.sport)
## Track and Field

This ensures that all instances of track athletes have ‘Track and Field’ listed as their sport.

1.3 Methods

As demonstrated previously, we can create an attribute for an object using ‘dot notation’:

athlete1.name = 'Usain Bolt'
print(athlete1.name)
## Usain Bolt

We could also do this with a function. Here’s a function called “set_name” which sets the “name” attribute of a TrackAthlete instance that is passed to it (in fact, it would set the “name” attribute of any instance that is passed to it):

def set_name(self, athlete_name):
    """Give the athlete a name."""
    self.name = athlete_name


# Call the function
set_name(athlete1, 'Elaine Thompson-Herah')
print(athlete1.name)
## Elaine Thompson-Herah

These two methods of setting the “name” attribute are not fundamentally different from each other, one is just inside a function while the other is outside of one. However, there’s a special third way to do this with classes: a ‘method’. A ‘method’ is essentially a function that’s unique to a class and is defined as part of it:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create a member variable
    sport = 'Track and Field'

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name


# Create an instance of the TrackAthlete class
athlete1 = TrackAthlete()
# Call a method (using 'dot notation' and with the first argument omitted)
athlete1.set_name('Usain Bolt')
print(athlete1.name)
## Usain Bolt

What’s the difference between a method (a ‘function inside a class’) and a regular function (a ‘function outside a class’)? Well, firstly, a method only works with objects of that specific class (whereas a regular function would work with an object of any class). Secondly, a method can be called using ‘dot notation’ (similar to an attribute or a member variable) and the name of the instance (ie the first argument, “self”) can be omitted when you do so (in fact, it MUST be omitted) because Python already knows what instance the method is being called on.

Calling this “set_name” method again or editing the attribute directly will overwrite the “name” attribute:

# Call the method again
athlete1.set_name('Shelly-Ann Fraser-Pryce')
print(athlete1.name)
## Shelly-Ann Fraser-Pryce
# Edit the member variable directly
athlete1.name = 'Marcell Jacobs'
print(athlete1.name)
## Marcell Jacobs

1.4 See What Attributes and Methods Exist

You can see what attributes and methods an object has by using the dir() function:

print(dir(athlete1))
## ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'set_name', 'sport']

The last three of these (‘name’, ‘set_name’ and ‘sport’) we expected because we created them ourselves, but where did all the others come from? These are default attributes and methods - 26 in total - created by Python automatically when you defined the TrackAthlete class. It will do this for any new class because Python needs these attributes and methods when handling objects, but you as a user shouldn’t have to worry about defining 26 methods just to get Python to function properly so it does it for you! As a convention, default methods’ names start and end with two underscores (and these punctuation marks get pronounced as ‘dunder’: starting and ending with double underscores). As a result, when you create new custom methods you should not use this convention, otherwise it could cause confusion as to which methods are default ones and which are custom ones. Most of the default methods can be ignored, but we will look at two in Section 2:

  • __init__ (pronounced ‘dunder init’)
  • __repr__ (pronounced ‘dunder rep-er’)

1.5 Attributes and Methods of Built-In Object Types

Let’s use dir() to see what attributes and methods exist for, as an example, strings:

st = 'Hello, World'
print(dir(st))
## ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__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', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

We can see lots of default methods named according to the ‘dunder’ convention, but then there are plenty of others too. Many of these were discussed on the strings page: .index(), .replace(), .removeprefix(), .removesuffix(), etc. So this is a good way to see all the things you can do with strings!

2 More Methods

2.1 A Method with a Return

Much like a regular function, a method can have a return:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create a member variable
    sport = 'Track and Field'

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        return self.name


# Create an instance of the TrackAthlete class
athlete1 = TrackAthlete()
# Call the set_name method
athlete1.set_name('Usain Bolt')
# Call the get_name method (necessarily without the first (and only) argument)
# and assign the return to a variable
athletes_name = athlete1.get_name()
print(athletes_name)
## Usain Bolt

2.2 The Initialisation Method

One of the default methods in Python is called __init__ (pronounced ‘dunder init’ where ‘dunder’ is short for ‘double underscore’) which, as mentioned above, gets created automatically when a new class is defined. It determines what happens when a new instance of that class is created (ie when a new instance is initialised) and, usually, it doesn’t do anything. However, we can overwrite its functionality in order to get it to do something useful:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create a member variable
    sport = 'Track and Field'

    # Edit the initialisation method
    def __init__(self, athlete_event):
        """Specify the athlete's event from an input parameter."""
        self.event = athlete_event

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        return self.name


# Create an instance of the TrackAthlete class but now with an input
athlete1 = TrackAthlete('100m')
# Get the athlete's event
athletes_event = athlete1.event
print(athletes_event)
## 100m

As you can see above, when the instance was created an input was given ('100m'). This is because the __init__ method requires an additional input (besides self which, as mentioned above, you must omit as an argument) called athlete_event which then gets assigned to an attribute called event.

Initialisation methods can be used to ensure that all instances of a class type have a particular attribute populated. In this example, all track athletes need to have the event they do specified.

2.3 The Representation Method

Similar to __init__, there is a default method called __repr__ (‘dunder rep-er’) which all classes have. This method controls how the instance is represented when it is printed, and can also be overwritten:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create a member variable
    sport = 'Track and Field'

    # Edit the initialisation method
    def __init__(self, athlete_event):
        """Specify the athlete's event from an input parameter."""
        self.event = athlete_event

    # Edit the representation method
    def __repr__(self):
        """Specify how this athlete is represented when printed."""
        return f'This athlete is {self.name} and they do the {self.event}'

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        return self.name


# Create an instance of the TrackAthlete class, specifying their event
athlete1 = TrackAthlete('100m')
# Call the set_name method
athlete1.set_name('Usain Bolt')
# Print the instance to see how it's represented
print(athlete1)
## This athlete is Usain Bolt and they do the 100m

HOWEVER, the way this has been coded assumes that the name attribute exists, which might not always be the case:

athlete2 = TrackAthlete('100m')
print(athlete2)
AttributeError: 'TrackAthlete' object has no attribute 'name'

So here’s an edit that first does some checks to prevent this type of problem from happening:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create a member variable
    sport = 'Track and Field'

    # Edit the initialisation method
    def __init__(self, athlete_event):
        """Specify the athlete's event from an input parameter."""
        self.event = athlete_event

    # Edit the representation method
    def __repr__(self):
        """Specify how this athlete is represented when printed."""
        if hasattr(self, 'name'):
            return f'This athlete is {self.name} and they do the {self.event}'
        else:
            return f"This unnamed athlete's event is the {self.event}"

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        if hasattr(self, 'name'):
            return self.name
        else:
            return 'This athlete does not yet have a name...'


# Create an instance of the TrackAthlete class, specifying their event
athlete2 = TrackAthlete('100m')
# Print the instance to see how it's represented
print(athlete2)
## This unnamed athlete's event is the 100m

2.4 A List as an Attribute

Having an attribute be a list is useful for storing information that can be appended to over time, such as an athlete’s race results:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create member variables
    sport = 'Track and Field'
    results = []

    # Edit the initialisation method
    def __init__(self, athlete_event):
        """Specify the athlete's event from an input parameter."""
        self.event = athlete_event

    # Edit the representation method
    def __repr__(self):
        """Specify how this athlete is represented when printed."""
        if hasattr(self, 'name'):
            return f'This athlete is {self.name} and they do the {self.event}'
        else:
            return f"This unnamed athlete's event is the {self.event}"

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        if hasattr(self, 'name'):
            return self.name
        else:
            return 'This athlete does not yet have a name...'

    # Create a method
    def add_result(self, result):
        """Record a result for this athlete."""
        self.results.append(result)


# Create an instance of the TrackAthlete class, specifying their event
athlete1 = TrackAthlete('100m')
# Add a race result for this athlete
athlete1.add_result(10.00)
# Add another race result for this athlete
athlete1.add_result(9.90)
# Show all race results
results = athlete1.results
print(f"This athlete's results for the {athlete1.event} are: {results}")
## This athlete's results for the 100m are: [10.0, 9.9]

2.5 A Method that Performs a Calculation

Now that we are storing a list of the athlete’s results, we can perform a calculation on the data. For example, we could calculate their fastest time and set it as their ‘personal best’ attribute:

class TrackAthlete:
    """A Python object representing a track athlete."""

    # Create member variables
    sport = 'Track and Field'
    results = []

    # Edit the initialisation method
    def __init__(self, athlete_event):
        """Specify the athlete's event from an input parameter."""
        self.event = athlete_event

    # Edit the representation method
    def __repr__(self):
        """Specify how this athlete is represented when printed."""
        if hasattr(self, 'name'):
            return f'This athlete is {self.name} and they do the {self.event}'
        else:
            return f"This unnamed athlete's event is the {self.event}"

    # Create a method
    def set_name(self, athlete_name):
        """Give the athlete a name."""
        self.name = athlete_name

    # Create a method
    def get_name(self):
        """Get the athlete's name."""
        if hasattr(self, 'name'):
            return self.name
        else:
            return 'This athlete does not yet have a name...'

    # Create a method
    def add_result(self, result):
        """Record a result for this athlete."""
        self.results.append(result)

    # Create a method that performs a calculation
    def personal_best(self):
        """Calculate the athlete's personal best."""
        return min(self.results)


# Create an instance of the TrackAthlete class, specifying their event
athlete1 = TrackAthlete('100m')
# Add a race result for this athlete
athlete1.add_result(10.00)
# Add another race result for this athlete
athlete1.add_result(9.90)
# Calculate and return their personal best
pb = athlete1.personal_best()
print(f"This athlete's personal best is {pb} seconds for the {athlete1.event}")
## This athlete's personal best is 9.9 seconds for the 100m

3 Parent and Child Classes

Some track athletes are Olympians, but not all of them. So if we wanted to create a Python class to store the data of Olympic track athletes it would have to be separate from the TrackAthlete class we’ve been using above (which can be used for any athlete). But we would still want it to have all the functionality of the TrackAthlete class - Olympic track athletes are track athletes after all. The solution is to create a class that inherits all of the functionality of TrackAthlete but then has some functionality of its own as well. This is known as a child class where TrackAthlete is the parent class.

To create a child class, create a class as per normal but with the parent class’s name inside round brackets before the colon:

class OlympicTrackAthlete(TrackAthlete):
    """A Python object representing an Olympic track athlete."""

    # Create a member variable
    medals = []

    # Create a method
    def add_medal(self, medal):
        """Record a medal won by this athlete."""
        self.medals.append(medal)


# Create an instance of the OlympicTrackAthlete class, specifying their event
athlete1 = OlympicTrackAthlete('100m')
# Call a method from the parent class
athlete1.set_name('Elaine Thompson-Herah')
# See the athlete's name
print(f"This athlete's name is {athlete1.name}")
## This athlete's name is Elaine Thompson-Herah
# Call a method from the child class
athlete1.add_medal('Gold')
# See the athlete's medals
print(f'This athlete has won the following medals: {athlete1.medals}')
## This athlete has won the following medals: ['Gold']

This instance has all the functionality of a TrackAthlete instance plus the ability to record what medals have been won.

3.1 Overwrite a Parent Method

If you aren’t content with a child class having more functionality than its parent class, you can go so far as to overwrite the parent’s methods. This is done by simply defining a method with the same name as one of the parent’s methods inside of the child’s definition:

class OlympicTrackAthlete(TrackAthlete):
    """A Python object representing an Olympic track athlete."""

    # Create member variables
    medals = []
    results = []

    # Create a method
    def add_medal(self, medal):
        """Record a medal won by this athlete."""
        self.medals.append(medal)

    # Create a method that overwrites a parent method
    def add_result(self, result, olympics):
        """Record a result from the Olympics for this athlete."""
        self.results.append((result, olympics))


# Create an instance of the OlympicTrackAthlete class, specifying their event
athlete1 = OlympicTrackAthlete('100m')
# Call a method from the child class (originally from the parent class)
athlete1.add_result(10.61, 'Tokyo 2020')
# See the athlete's results and which Olympics they come from
print(f"This athlete's results: {athlete1.results}")
## This athlete's results: [(10.61, 'Tokyo 2020')]

3.2 Use a Super-Call

The parent class is also called the super-class and the child class is also called the sub-class.

If you’ve overwritten a method from the super-class in the sub-class you can actually still use the original method. This is known as a super-call. It’s done by using the super() function and giving it the name of the sub-class along with self (so Python knows you are referring to an instance of the type currently being defined), after which you can use the method from the super-class as per normal:

class OlympicTrackAthlete(TrackAthlete):
    """A Python object representing an Olympic track athlete."""

    # Create member variables
    medals = []
    results = []

    # Create a method
    def add_medal(self, medal):
        """Record a medal won by this athlete."""
        self.medals.append(medal)

    # Create a method that overwrites a parent method
    def add_result(self, result, olympics):
        """Record a result from the Olympics for this athlete."""
        self.results.append((result, olympics))

    # Make a super-call
    # (use a method from the super-class that's been overwritten in the
    # sub-class)
    def add_non_olympic_results(self, result):
        """Record a result not from the Olympics for this athlete."""
        super(OlympicTrackAthlete, self).add_result(result)


# Create an instance of the OlympicTrackAthlete class, specifying their event
athlete1 = OlympicTrackAthlete('100m')
# Add a race result (not from the Olympics) for this Olympic athlete
athlete1.add_non_olympic_results(10.76)
# View this athlete's results
print(f"This athlete's results not from the Olympics are: {athlete1.results}")
## This athlete's results not from the Olympics are: [10.76]

4 Summary

  • Classes are custom object types
  • Instances of a class have the functionality of that class and can also store data
  • Inside the class definition, you can define attributes, member variables and methods for that class
  • Methods are essentially functions that are unique to instances of that class
  • There are some special methods that are auto-created but which can be overwritten, such as __init__ and __repr__
  • You can have parent and child classes (super- and sub-classes) where the child class inherits everything from its parent class and extends it

⇦ Back