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.
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
voideither. Python has no return type on the constructor at all. - There is no
newkeyword.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.
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:
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:
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:
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.
Instance, class, local and global variables
Four scopes turned up in one file, and telling them apart is a standing interview question:
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.
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.
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:
pip install python-dotenv
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.
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 |
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.
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.
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:
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.
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.
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.
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.