Python

Constructors, Variables, and Methods: The Building Blocks of Every Python Class

self, cls, __init__, @staticmethod — if Python classes still feel like a jumble of syntax, this guide breaks down constructors, instance/class variables, and all three method types with clear comparisons and a decision framework.

Python OOP Explained: (A Beginner-to-Intermediate Guide)

If you’ve ever opened a Python class and felt overwhelmed by self, cls, __init__, and a handful of decorators, you’re not alone. Object-Oriented Programming (OOP) in Python looks intimidating at first, but once you understand the role each piece plays, it clicks into place surprisingly fast.

In this post, we’ll walk through six foundational building blocks of Python classes — constructors, instance variables, class variables, instance methods, class methods, and static methods — and tie them together with practical comparisons and a decision framework you can actually use while coding.

Let’s build this up piece by piece, the way you’d build a real class.


The Constructor (__init__)

What is it?

A constructor is a special method that Python runs automatically the moment you create a new object. Think of it as the “setup crew” that walks into an empty room and arranges the furniture before anyone moves in.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

emp1 = Employee("Riya", 55000)

The moment Employee("Riya", 55000) runs, Python calls __init__ behind the scenes and sets up emp1 with a name and a salary — no extra steps needed.

Why does it matter?

Without a constructor, you’d have to create an empty object and then manually assign every attribute afterward — error-prone and easy to forget. The constructor guarantees that every object starts life in a complete, usable state.

Key characteristics

  • Runs automatically when an object is created.
  • Executes exactly once per object.
  • Initializes instance variables.
  • Accepts parameters, including optional ones with default values.
  • Never returns a value (it always returns None implicitly).

Best practices

  • Initialize every attribute the object needs right there in __init__.
  • Keep it lightweight — no database calls, API requests, or heavy file processing inside a constructor.
  • Only use default values when they genuinely make sense (e.g., status="active"), not as a lazy placeholder.

Rule of thumb: if creating an object feels “slow” or “risky,” it’s usually because the constructor is doing too much.


Instance Variables

What are they?

Instance variables are data that belongs to one specific object. Two objects from the same class can hold completely different values for the same attribute.

emp1 = Employee("Riya", 55000)
emp2 = Employee("Aman", 62000)

print(emp1.salary)  # 55000
print(emp2.salary)  # 62000

emp1 and emp2 are both Employee objects, but each carries its own independent copy of name and salary.

Why do we need them?

Real-world entities modeled by the same class are rarely identical. A Car class might produce a red hatchback and a blue SUV — same blueprint, different details. Instance variables capture that individuality.

Key characteristics

  • Declared using self.variable_name.
  • Stored separately inside each object.
  • Changing one object’s instance variable never affects another object.
  • They live as long as the object itself does.

Best practices

  • Use instance variables only for data that’s genuinely specific to that object (IDs, names, balances, scores).
  • Initialize them in the constructor so every object starts complete.
  • Always access and modify them through self inside the class.

Class Variables

What are they?

Class variables belong to the class itself, not to any single object. Every object sees the same shared value, unless it’s explicitly overridden.

class Employee:
    company_name = "TechNova"  # class variable

    def __init__(self, name, salary):
        self.name = name       # instance variable
        self.salary = salary

emp1 = Employee("Riya", 55000)
emp2 = Employee("Aman", 62000)

print(emp1.company_name)  # TechNova
print(emp2.company_name)  # TechNova

Both employees work at the same company — there’s no reason to store "TechNova" separately in every object. That would waste memory and risk inconsistency if the company name ever changes.

Why do we need them?

Some data doesn’t vary between objects — it’s shared context. Instead of duplicating it in every instance, a class variable stores it once.

Key characteristics

  • Declared directly inside the class body (not inside __init__).
  • Shared by every object created from that class.
  • Only one copy exists in memory, no matter how many objects you create.
  • Can be accessed via the class name or via any object.

Best practices

  • Reserve class variables for truly common data: configuration values, constants, counters, shared settings.
  • Modify them through the class name (Employee.company_name = "NewCo"), not through an individual object, to avoid confusing bugs.
  • Never store per-object data here — that’s what instance variables are for.

Common pitfall: if you accidentally do emp1.company_name = "StartUp", Python creates a new instance variable on emp1 that shadows the class variable — it doesn’t change the shared value for everyone else. This trips up a lot of beginners, so it’s worth testing in your own interpreter to see it happen.


Instance Methods

What are they?

Instance methods define what an individual object can do. They’re the verbs attached to your nouns (objects), and they can read or change that object’s own data.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def give_raise(self, amount):
        self.salary += amount
        return self.salary

emp1 = Employee("Riya", 55000)
emp1.give_raise(5000)
print(emp1.salary)  # 60000

Why do we need them?

Objects shouldn’t just sit there holding data — they should be able to act on it. give_raise() is behavior that naturally depends on this specific employee’s salary.

Key characteristics

  • Always take self as the first parameter (Python passes this automatically).
  • Can access and modify both instance variables and class variables.
  • Called on an object: emp1.give_raise(5000).

Best practices

  • Use instance methods whenever the behavior depends on that object’s own data.
  • Keep each method focused on one clear responsibility — a method that does five unrelated things is a warning sign.
  • Name methods with verbs that describe the action: calculate_bonus(), update_email(), deactivate_account().

Most of the actual business logic in real applications lives here, because most behavior genuinely depends on individual object state.


Class Methods

What are they?

Class methods operate on the class rather than any one object. Instead of self, they receive cls — a reference to the class itself.

class Employee:
    company_name = "TechNova"

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data["salary"])

emp_data = {"name": "Neha", "salary": 48000}
emp3 = Employee.from_dict(emp_data)
print(emp3.name)  # Neha

Why do we need them?

Some operations affect the whole class, or offer an alternative way to build an object — for example, constructing an Employee from a dictionary instead of separate arguments.

Key characteristics

  • Declared with the @classmethod decorator.
  • Receive cls as the first parameter automatically.
  • Typically work with class variables rather than instance-specific data.
  • Can create and return new objects (this is what makes “alternative constructors” possible).

Best practices

  • Use class methods to read or update shared, class-level information.
  • Build alternative constructors like from_json(), from_csv(), or from_dict() — this is one of the most common real-world uses.
  • Prefer cls over hardcoding the class name, so subclasses inherit the behavior correctly.

Why cls instead of Employee directly? If someone later creates a subclass, say Manager(Employee), calling Manager.from_dict(data) with cls(...) correctly returns a Manager object — not an Employee. Hardcoding the class name would break that flexibility.


Static Methods

What are they?

Static methods are utility functions that live inside a class because they’re conceptually related to it — but they don’t touch object data (self) or class data (cls) at all.

class Employee:
    @staticmethod
    def is_valid_salary(amount):
        return amount > 0

print(Employee.is_valid_salary(55000))  # True
print(Employee.is_valid_salary(-100))   # False

Why do we need them?

Some logic just belongs near a class conceptually — like validating a salary before you even create an Employee — but it doesn’t need any object or class state to do its job. Static methods keep related helper logic organized instead of scattering loose functions everywhere.

Key characteristics

  • Declared with the @staticmethod decorator.
  • Receive no automatic first argument — no self, no cls.
  • Behave exactly like a regular function, just namespaced inside the class.
  • Callable through either the class or an instance.

Best practices

  • Use them for validation, formatting, calculations, or conversions that relate to the class but don’t need its data.
  • Avoid reaching for a static method when you actually need instance or class state — that’s a sign it should be an instance or class method instead.
  • Keep them small, focused, and reusable.

Instance Variables vs. Class Variables

Instance Variables Class Variables
Belong to an individual object Belong to the class
Every object has its own copy All objects share one copy
Store unique object data Store common shared data
Created using self Declared directly inside the class
Changes affect only one object Changes affect every object

Instance Methods vs. Class Methods vs. Static Methods

Instance Method Class Method Static Method
Operates on a specific object Operates on the class Independent of object and class state
Receives self Receives cls Receives no automatic parameter
Accesses instance and class variables Primarily accesses class variables Accesses neither unless explicitly passed
Modifies object state Modifies shared state or creates objects Performs utility operations
Used for object behavior Used for class-level behavior and factory methods Used for helper functions related to the class

How to Decide What to Use

When you’re building a class and unsure which tool fits, ask these questions in order:

  1. Does every object need its own copy of this data? → Use an instance variable.
  2. Is this data shared by every object? → Use a class variable.
  3. Does this behavior depend on a specific object’s state? → Use an instance method.
  4. Does this behavior affect the entire class, or create objects in different ways? → Use a class method.
  5. Is this simply a helper function related to the class? → Use a static method.
  6. How should every object start its life? → Initialize that state inside the constructor.

Keep this checklist nearby — most design confusion in early OOP code comes from skipping straight to writing methods without asking these questions first.


Putting It All Together: The OOP Lifecycle

A well-designed Python class typically follows this sequence:

  1. Define the class to represent a real-world entity.
  2. Declare class variables for information shared across all objects.
  3. Write the constructor to initialize every new object with a valid state.
  4. Create instance variables to store data unique to each object.
  5. Implement instance methods to define object-specific behavior.
  6. Add class methods for shared operations and alternative object creation.
  7. Include static methods for utility functions closely related to the class.
  8. Create objects, which then interact through these methods while sharing common class-level data where appropriate.

Putting It All Together: A Complete Example

Let’s tie every concept from this post — the constructor, instance variables, class variables, instance methods, class methods, and static methods — into a single working Student class.

class Student:
    """
    Student class demonstrating:
    - Constructor
    - Instance Variables
    - Class Variables
    - Instance Methods
    - Class Methods
    - Static Methods
    """

    # ==========================
    # CLASS VARIABLES
    # ==========================
    college_name = "Lovely Professional University"
    total_students = 0

    # ==========================
    # CONSTRUCTOR
    # ==========================
    def __init__(self, name, age, course):
        print(f"Creating Student Object -> {name}")

        # Instance Variables
        self.name = name
        self.age = age
        self.course = course

        # Default values
        self.marks = []
        self.cgpa = 0.0

        # Increase class counter
        Student.total_students += 1

    # ==========================
    # INSTANCE METHODS
    # ==========================
    def add_marks(self, mark):
        """Add a single subject mark"""
        self.marks.append(mark)

    def calculate_cgpa(self):
        """Calculate average marks"""
        if self.marks:
            self.cgpa = sum(self.marks) / len(self.marks)
        return self.cgpa

    def display(self):
        """Display student details"""
        print("\n------ Student Details ------")
        print("Name      :", self.name)
        print("Age       :", self.age)
        print("Course    :", self.course)
        print("College   :", Student.college_name)
        print("Marks     :", self.marks)
        print("CGPA      :", self.cgpa)

    # ==========================
    # CLASS METHOD
    # ==========================
    @classmethod
    def change_college(cls, new_name):
        """Change college for every student"""
        cls.college_name = new_name

    @classmethod
    def from_string(cls, data):
        """
        Alternative Constructor
        Input:
        "Rahul,22,MTech"
        Output:
        Student Object
        """
        name, age, course = data.split(",")
        return cls(name, int(age), course)

    # ==========================
    # STATIC METHODS
    # ==========================
    @staticmethod
    def is_valid_age(age):
        return age >= 18

    @staticmethod
    def calculate_percentage(obtained, total):
        return (obtained / total) * 100


# ==========================================
# PROGRAM STARTS HERE
# ==========================================
print("\nCreating Students...\n")
s1 = Student("Joban", 23, "MTech AI")
s2 = Student("Aman", 21, "BTech CSE")

print("\nTotal Students :", Student.total_students)

# ------------------------------------------
# Instance Method
# ------------------------------------------
s1.add_marks(85)
s1.add_marks(90)
s1.add_marks(80)

s2.add_marks(70)
s2.add_marks(75)
s2.add_marks(68)

s1.calculate_cgpa()
s2.calculate_cgpa()

s1.display()
s2.display()

# ------------------------------------------
# Class Method
# ------------------------------------------
print("\nChanging College Name...\n")
Student.change_college("OpenAI University")

s1.display()
s2.display()

# ------------------------------------------
# Alternative Constructor
# ------------------------------------------
print("\nCreating Student From String...\n")
s3 = Student.from_string("Rohan,24,MCA")
s3.add_marks(95)
s3.add_marks(96)
s3.calculate_cgpa()
s3.display()

print("\nTotal Students :", Student.total_students)

# ------------------------------------------
# Static Method
# ------------------------------------------
print("\nAge Validation")
print(Student.is_valid_age(16))
print(Student.is_valid_age(25))

print("\nPercentage")
print(Student.calculate_percentage(450, 500))

One-Line Mental Model

A class defines the blueprint, the constructor prepares each object, instance variables store its unique state, class variables hold shared information, instance methods describe what an object can do, class methods manage class-wide behavior or create objects in alternative ways, and static methods provide utility functions that belong conceptually to the class without depending on its state.

Once these pieces settle into place, reading (and writing) production-quality Python classes stops feeling like memorizing syntax — it starts feeling like design. That shift is what separates beginner code from maintainable, scalable, professional code.


Next up: we’ll dive into inheritance, super(), and how Python resolves method calls across parent and child classes — the natural next step once constructors and methods feel solid.