The Testing Academy · Class Notes Sunday, 9 August (IST)
Live class · study guide

Python OOP: constructors, encapsulation, and the five kinds of inheritance

How __init__ runs the moment an object is created, why self is the first parameter of every method, what the underscore naming convention really buys you, and all five kinds of inheritance including the one Python solved with MRO.

By Pramod Dutta, The Testing Academy. Study notes from the live Python class, rebuilt from the session recording. Code is reproduced as it was written on screen, with the output the class saw.

01

Constructors run the moment an object is created

A constructor is a special function that Python calls automatically when you create an object. In Python it always carries the same name, __init__, unlike C++ or Java where the constructor takes the class name.

Python
class MobilePhone:
    model = None

    def __init__(self):
        print("Constructor called")

    def talk(self):
        print("Talking")

iphone = MobilePhone()   # Constructor called

The object is created on the last line, and __init__ fires on its own. Three details that get asked in interviews:

  • It returns nothing, and it is not void either. Python has no return type on the constructor at all.
  • There is no new keyword. MobilePhone() is the whole syntax.
  • Its real job is initialising attributes. Anything the object needs a value for gets one here.

The analogy from class: when a baby is born you give it a name straight away. You do not register the birth with the name left as None. The constructor is that naming moment for an object.

Python does not need semicolons at the end of a statement, so leave them out.

02

self, and the two kinds of constructor

self is the first parameter of every method in the class, and it points at the object the method was called on. It is what this is in Java, JavaScript or TypeScript. Attributes are reached through it: self.model, never bare model.

A default constructor takes no parameters beyond self. A parameterized constructor takes the values the object should start with:

Python
class Dog:
    def __init__(self, name_given, breed_given):
        self.name = name_given
        self.breed = breed_given

    def bark(self):
        print("Who is barking?", self.name)

chow = Dog("Chow", "Mastiff")
desi = Dog("Rancho", "Desi")

chow.bark()   # Who is barking? Chow
desi.bark()   # Who is barking? Rancho

Two objects, two independent sets of attributes. chow and desi point at different objects in memory, so the values never collide.

Leave the arguments out and Python raises an error, because there is no second, no-argument constructor to fall back on:

Python
stray = Dog()   # TypeError: __init__() missing 2 required positional arguments

A Python class holds exactly one __init__. Write a second one and it simply replaces the first, which is why the Java habit of overloading constructors does not carry over. When you want both shapes, give the parameters default values: def __init__(self, name_given=None, breed_given=None):.

An object whose attributes were never initialised prints None, and None is not null. Same idea, different keyword, and Python spells it with a capital N.

Taking user input inside a class works fine:

Python
class Student:
    def __init__(self):
        self.name = input("Enter your name: ")
        self.age = input("Enter your age: ")

The VS Code Code Runner extension cannot feed input to a running program, so anything with input() looks like it hangs. Right-click the file and choose Run Python File in Terminal instead.

03

Instance, class, local and global variables

Four scopes turned up in one file, and telling them apart is a standing interview question:

Python
A = 10                      # global, visible everywhere in the module

class Demo:
    B = 11                  # class variable, visible throughout the class

    def print_info(self):
        L = 12              # local, dies when the method returns
        print(self.B)       # 11
        print(A)            # 10
Variable Where it lives Reachable from
A module level anywhere in the file, including inside methods
B class body any method in the class, through self.B
L inside a method that method only

The advantage of a local variable is exactly that limit: it cannot be read or damaged from outside.

Static variables are a separate topic and were deliberately left for a later session. Do not assume the class variable above behaves the same way.

04

Encapsulation: hide the attributes, expose the methods

Encapsulation means binding the data to the methods that operate on it, then hiding the data so it can only be reached through those methods.

The class started from the counter-example: a login class that stores a password as a plain attribute.

Python
class LoginPage:
    def __init__(self, email, password):
        self.email = email
        self.password = password

page = LoginPage("qa@example.com", "secret123")
print(page.password)   # secret123, and that is the problem

The password is readable from anywhere in the program. That is encapsulation broken.

Credentials moved out to a .env file first, using the python-dotenv library:

Terminal
pip install python-dotenv
Python
import os
from dotenv import load_dotenv

load_dotenv()
username = os.getenv("USERNAME")
password = os.getenv("PASSWORD")

The .env file is never committed to GitHub and never shared. It plays the role a properties file plays in other stacks, with the difference that it stays out of version control.

05

Public, protected, private, by naming convention

Python has no access-modifier keywords. The level is signalled by how you name the attribute:

Level Written as Reachable from
Public name anywhere
Protected _name the class and its subclasses, by convention
Private __name inside the class only
Python
class BankAccount:
    def __init__(self, balance, account_number):
        self.balance = balance                  # public, fine to read
        self.__account_number = account_number  # private

    def get_account_number(self):               # the only door in
        return self.__account_number

acc = BankAccount(5000, "XXXX1234")
print(acc.balance)                # 5000
print(acc.get_account_number())   # XXXX1234
print(acc.__account_number)       # AttributeError

The analogy from class: your house, your society, and the street outside. Small children (private) stay inside the house. Older children (protected) can visit other homes in the same society. Adults (public) go anywhere. And a child only goes out accompanied by the mother, which is the getter method.

An automation example from the same idea: the driver is public, the configuration is protected, the API key is private, and a public method can still reach all three from inside the class.

The double underscore is name mangling rather than a hard lock. Python rewrites __account_number to _BankAccount__account_number, so a determined caller can still reach it. Treat it as private anyway: the convention is what your team and your linters enforce.

06

Inheritance and its five types

Inheritance lets one class take the attributes and behaviour of another. The giver is the parent or base class, the receiver is the child or subclass. There is no extends keyword: the parent goes in brackets after the class name.

Python
class BaseTest:
    driver = "chrome"

    def setup(self):
        print("Base setup done")

class LoginTest(BaseTest):
    def run(self):
        self.setup()                    # parent method, called as if it were ours
        print("Running on", self.driver)

LoginTest().run()
# Base setup done
# Running on chrome

All five types are supported:

Type Shape Family version
Single one parent, one child father to son
Multiple two or more parents, one child two fathers to one son
Multi-level parent, child, grandchild grandfather to father to son
Hierarchical one parent, several children one father, several children
Hybrid hierarchical plus multiple combined base splits to two, both rejoin in one child

Multiple inheritance is written with a comma:

Python
class ApiBase:
    def api_auth(self):
        print("API auth")

class DbBase:
    def db_connect(self):
        print("DB connect")

class ApiTest(ApiBase, DbBase):
    pass

t = ApiTest()
t.api_auth()     # API auth
t.db_connect()   # DB connect

Java, JavaScript and C# do not allow a class to inherit from multiple classes, which is why the pattern feels unfamiliar coming from those languages. They reach the same goal through interfaces. Python allows it directly and resolves the ambiguity with MRO, below.

07

MRO: which parent wins

The hard case is two parents defining the same name. Python resolves it with the method resolution order, and the order you list the parents in decides the answer.

Python
class Father1:
    def money(self):
        print("F1 money")

class Father2:
    def money(self):
        print("F2 money")

class Son(Father2, Father1):
    pass

Son().money()   # F2 money

Swap the brackets to class Son(Father1, Father2) and the output becomes F1 money. Same code, different resolution, decided entirely by the listed order.

Declaration Lookup order Son().money()
class Son(Father2, Father1) Son, Father2, Father1 F2 money
class Son(Father1, Father2) Son, Father1, Father2 F1 money

Left-to-right is the right mental model for simple hierarchies, and the real rule is an algorithm called C3 linearization, which also keeps each parent ahead of its own parents. When a hybrid tree gets confusing, ask Python instead of guessing: print(Son.__mro__) prints the exact order it will search.

Hybrid inheritance is where this earns its keep: a base class splits into two children, both of which are inherited by a single grandchild, and MRO is what keeps that unambiguous.

Given two parent classes that both define money(), which one runs, and what single change flips the answer? Name the mechanism.

08

Homework and announcements

  • Yesterday's task is still open for most of the batch. Finish it today.
  • 15 August: holiday.
  • 16 August: hackathon, running 11:00 to 23:00 IST. Submissions are compulsory. Instructions come from Deepak and Meethi on the day. Everyone who submits gets a Testing Academy certificate, and the top five share prizes of up to 5,000 rupees.
  • Skills masterclass revision, including MCP creation: Wednesday 12 August, 8:00 PM IST. Moved off Thursday.
  • Coming next: the last part of Python, then DeepEval, CrewAI and LangChain.
  • Evening masterclasses continue on Tuesdays and Fridays through the end of the year.
  • A doubt thread for the OOP topics is open. Today's code is pushed to the repository.

Abstraction, polymorphism, exception handling and static members were named as part of this OOP series and have not been covered yet. They are coming in the remaining Python sessions.