__init__) to create consistent objects.đ§ Activities spaced throughout the session
Object-Oriented Programming (OOP) is a programming approach that bundles data and functionality together into âobjectsâ. This can help manage complexity in larger codebases by organizing related data and behavior.
A class is a template for creating objects. It defines the attributes (data) and methods (functions) that the objects will have. In Python, we use the class keyword to define a class.
Here weâve defined a class with one method: __init__. This method is the initialiser method, which is responsible for setting up the initial values and structure of the data inside a new instance of the class - this is very similar to constructors in other languages, so the term is often used in Python too. The __init__ method is called every time we create a new instance of the class. The __init__ method ensures that object construction is consistent by providing shared initialization rules for all objects.
The argument self refers to the instance on which we are calling the method and gets filled in automatically by Python. We can use self to assign attributes to the object. Data encapsulated within our Patient class includes the patient ID, and a list of their inflammation values. In the initialiser method, we set an objectâs attributes to the values provided. Attributes are typically hidden (encapsulated) internal object details ensuring that access to data is protected from unintended changes. They are manipulated internally by the class, which, in addition, can expose certain functionality as public behavior of the class to allow other objects to interact with this classâ instances.
Once we have defined a class, we can create objects of that class. Each object can have its own unique data.
Tip
A class is a template for constructing objects. An object is one concrete instance of a class.
In addition to representing a piece of structured data (e.g. an object that has an id and the lists with timestamps and magnitude observations), a class can also provide a set of functions, or methods, which describe the behaviours of the data encapsulated in the instances of that class. To define the behaviour of a class we add functions which operate on the data the class contains. These functions are the member functions or methods.
Methods on classes are the same as normal functions, except that they live inside a class and have an extra first parameter self. Using the name self is not strictly necessary, but is a very strong convention - it is extremely rare to see any other name chosen. When we call a method on an object, the value of self is automatically set to this object - hence the name. As we saw with the __init__ method previously, we do not need to explicitly provide a value for the self argument, this is done for us by Python.
Why is the __init__ method not called init? There are a few special method names that we can use which Python will use to provide a few common behaviours, each of which begins and ends with a double-underscore, hence the name dunder method.
When writing your own Python classes, youâll almost always want to write an __init__ method, but there are a few other common ones you might need sometimes. You may have noticed in the code above that the method print(p1) returned <__main__.Patient object at 0x7fd7e61b73d0>, which is the string representation of the patient object. We may want the print statement to display the patientsâs id instead. We can achieve this by overriding the __str__ method of our class.
These dunder methods are not usually called directly, but rather provide the implementation of some functionality we can use - we didnât call patient.__str__(), but it was called for us when we did print(star). Some we see quite commonly are:
__str__ - converts an object into its string representation, used when you call str(object) or print(object).__getitem__ - Accesses an object by key, this is how list[x] and dict[x] are implemented__len__ - gets the length of an object when we use len(object) - usually the number of items it containsThere are many more described in the Python documentation, but itâs also worth experimenting with built in Python objects to see which methods provide which behaviour. For a more complete list of these special methods, see the Special Method Names section of the Python documentation.
Class and Static Methods
Sometimes, the function weâre writing doesnât need access to any data belonging to a particular object. For these situations, we can instead use a class method or a static method. Class methods have access to the class that theyâre a part of, and can access data on that class - but do not belong to a specific instance of that class, whereas static methods have access to neither the class nor its instances.
By convention, class methods use cls as their first argument instead of self - this is how we access the class and its data, just like self allows us to access the instance and its data. Static methods have neither self nor cls so the arguments look like a typical free function. These are the only common exceptions to using self for a methodâs first argument.
Both of these method types are created using decorators - for more information see the classmethod and staticmethod decorator sections of the Python documentation.
The final special type of method we will introduce is a property. Properties are methods which behave like data - when we want to access them, we do not need to use brackets to call the method manually.
You may recognise the @ syntax from the classmethod and staticmethod documentation above - property is another example of a decorator. In this case the property decorator is taking the last_observation function and modifying its behaviour, so it can be accessed as if it were a normal attribute. It is also possible to make your own decorators, but we wonât cover it here.
We now have a language construct for grouping data and behaviour related to a single conceptual object. The next step we need to take is to describe the relationships between the concepts in our code.
There are two fundamental types of relationship between objects which we need to be able to describe:
In object oriented programming, we can make things components of other things.
We often use composition where we can say âx has a yâ - for example, we might want to say that a trial has a patient. For our current class example, it will look like this:
Now weâre using a composition of two custom classes to describe the relationship between two types of entity in the system that weâre modelling. The benefit of this approach is that we can create a new class called e.g. Hospital, which has its own analysis methods that differ from those of the class Trial. The implementation of the Hospital class will be different, but it can still have Patients.
The other type of relationship used in object oriented programming is inheritance. Inheritance is about data and behaviour shared by classes, because they have some shared identity - âx is a yâ. If class X inherits from (is a) class Y, we say that Y is the superclass or parent class of X, or X is a subclass of Y.
Extending the previous example, we can have patients given a placebo, and patients given the treatment. TreatedPatients will benefit from an attribute representing the dose they have been assigned, while PlaceboPatients can have an attribute representing the type of placebo administered such as âsugar pillâ or âsalineâ to track experimental controls. Instead of writing these two classes completely independently, we can make them both the subclasses of the class Patient.
To write our class in Python, we used the class keyword, the name of the class, and then a block of the functions that belong to it. If the class inherits from another class, we include the parent class name in brackets.
In this example, Patient is a parent class (or superclass), and TreatedPatient is a subclass.
Thereâs something else we need to add as well - Python doesnât automatically call the __init__ method on the parent class if we provide a new __init__ for our subclass, so weâll need to call it ourselves. This makes sure that everything that needs to be initialised on the parent class has been, before we need to use it. If we donât define a new __init__ method for our subclass, Python will look for one on the parent class and use it automatically. This is true of all methods - if we call a method which doesnât exist directly on our class, Python will search for it among the parent classes. The order in which it does this search is known as the method resolution order.
The line super().__init__(patient_id, inflammation_values) gets the parent class, then calls the __init__ method, providing the patient_id and inflammation_values variables that Patient.__init__ requires. This is quite a common pattern, particularly for __init__ methods, where we need to make sure an object is initialised as a valid X, before we can initialise it as a valid Y - e.g. a valid Patient must have a patient_id, before we can properly initialise a TreatedPatient model with their data.
Composition vs Inheritance
When deciding how to implement a model of a particular system, you often have a choice of either composition or inheritance, where there is no obviously correct choice. For example, instead of creating TreatedPatient and PlaceboPatient classes that inherit from Patient, we could have introduced a TreatmentType class that stores the details of the treatment given to a patient. In this alternative design, a Patient would simply have a TreatmentType object associated with them, rather than creating specialized subtypes of patient.
Both of these would be perfectly valid models and would work for most purposes. However, unless thereâs something about how you need to use the model which would benefit from using a model based on inheritance, itâs usually recommended to opt for composition over inheritance. This is a common design principle in the object oriented paradigm and is worth remembering, as itâs very common for people to overuse inheritance once theyâve been introduced to it.
For much more detail on this see the Python Design Patterns guide.
Multiple Inheritance
Multiple Inheritance is when a class inherits from more than one direct parent class. It exists in Python, but is often not present in other Object Oriented languages.
Although this might seem useful, itâs best to avoid it unless youâre sure itâs the right thing to do, due to the complexity of the inheritance heirarchy. Often using multiple inheritance is a sign you should instead be using composition.
You may not have realised, but you should already be familiar with some of the classes that come bundled as part of Python, for example:
Lists, dictionaries and sets are a slightly special type of class, but they behave in much the same way as a class we might define ourselves:
The behaviours we may have seen previously include:
Use classes when you repeatedly need:
Prefer simple functions when logic is:
Tip
Choose the simplest design that stays maintainable.
Sample with fields: sample_id, group, value.is_treated() returning True when group == "treated".__init__ defines how objects are created.self.This module introduces object-oriented programming for structuring larger Python projects. Learners define classes, initialize object state, implement methods, and apply inheritance carefully when reuse is justified.
The concepts in this module connect directly to practical data handling and exploration in Python.
| Submodule | Python Connection | Why It Matters |
|---|---|---|
| Class Fundamentals | Classes | Classes organize related data and behavior in one unit. |
| Instance Methods | Method objects | Methods provide clear interfaces for repeated operations. |
| Inheritance | Inheritance | Inheritance supports reuse when shared behavior is stable. |
Attribution
This lesson is derived from materials developed by the Software Carpentry project.
The original content is licensed under the Creative Commons Attribution 4.0 International (CC BY 4.0) license: https://github.com/swcarpentry/python-novice-inflammation/blob/main/LICENSE.md