Python Classes and Objects

I keep coming back to Python classes because they are the foundation of every non-trivial program I write. After years of using them daily, I have a good sense of what actually helps someone grasp this material quickly and what just confuses people. This guide is the article I wish I had when I started.

Python classes are not complicated once you understand a few core ideas. You define a class to create a blueprint, then you instantiate it to get objects that hold data and can perform actions. That is the whole idea. Everything else in object-oriented programming is just building on top of this simple foundation.

TLDR

  • A Python class is a blueprint that defines attributes (data) and methods (functions) bundled together as a single unit
  • Instantiate a class by calling it like a function: emp = Employee(1001, "Alice")
  • The __init__ method initializes each new object and always takes self as the first parameter
  • Instance variables are unique to each object; class variables are shared across all instances
  • Use @classmethod for methods that need access to the class itself, and @staticmethod for utility functions that do not need any implicit argument

What Is a Python Class?

A class is a blueprint for creating objects. It bundles data (called attributes or properties) and functions (called methods) into a single unit. When you define a class, you are not creating any objects yet. You are just describing what those objects will look like and what they can do.

For example, imagine you run a company with employees. You could create an Employee class that describes every employee: each one has an ID number and a name, and each one can perform work. The class itself does not represent a specific employee. It is just the template you use to create actual employee objects.

Here is the simplest possible class in Python:




class Employee:
    pass



An empty class is not useful on its own, but it shows the basic syntax. You use the class keyword followed by the class name and a colon. Everything indented below belongs to the class.

Let me add some actual attributes and methods to make this class do something:




class Employee:
    company_name = "Acme Corp"

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

    def work(self):
        print(f"{self.name} (ID: {self.employee_id}) is working")

    def get_info(self):
        return f"{self.name} - {self.employee_id} - {self.company_name}"



How to Create an Object from a Class

Creating an object from a class is called instantiation. You call the class as if it were a function, and Python returns a new object that is an instance of that class.




emp1 = Employee(1001, "Alice")
emp2 = Employee(1002, "Bob")

emp1.work()
print(emp2.get_info())



The output looks like this:




Alice (ID: 1001) is working
Bob - 1002 - Acme Corp



Each object has its own identity. emp1 and emp2 are distinct objects, even though they come from the same class. Changing emp1’s name does not affect emp2’s name.

Understanding the __init__ Method

The __init__ method is a special method that Python calls automatically when you create a new object. People call it a constructor, which is mostly accurate, though technically Python does not have true constructors like Java or C++. The __init__ method sets up the object with initial values.

The first parameter of every method in a class is always self. self refers to the current instance of the class. When I call emp1.work(), Python passes emp1 as the self argument. This is how each object knows which data belongs to itself.

Here is what happens when you forget to pass the required arguments:




emp3 = Employee()



Running that code produces:




TypeError: __init__() missing 2 required positional arguments: 'employee_id' and 'name'



Default Values in __init__

You can give parameters default values by assigning them in the method signature. This lets callers omit arguments they do not want to specify:




class Employee:
    def __init__(self, employee_id, name, department="General"):
        self.employee_id = employee_id
        self.name = name
        self.department = department

    def get_info(self):
        return f"{self.name} (ID: {self.employee_id}) - {self.department}"

emp1 = Employee(1001, "Alice")
emp2 = Employee(1002, "Bob", "Engineering")

print(emp1.get_info())
print(emp2.get_info())



The output:




Alice (1001) - General
Bob (1002) - Engineering



Class Variables vs Instance Variables

There are two kinds of attributes in a Python class: class variables and instance variables.

Instance variables are defined inside __init__ and are unique to each object. In the Employee class, employee_id and name are instance variables. Each Employee object has its own copy of these values.

Class variables are defined directly inside the class but outside any method. They are shared across all instances of the class. In the Employee class, company_name is a class variable. Every Employee object sees the same value for company_name.

Here is an example that shows the difference clearly:




class Employee:
    company_name = "Acme Corp"
    total_employees = 0

    def __init__(self, employee_id, name):
        self.employee_id = employee_id
        self.name = name
        Employee.total_employees += 1

    def get_info(self):
        return f"{self.name} (ID: {self.employee_id}) at {self.company_name}"


print(f"Before hiring: {Employee.total_employees}")
emp1 = Employee(1001, "Alice")
emp2 = Employee(1002, "Bob")
print(f"After hiring: {Employee.total_employees}")

print(emp1.get_info())
print(emp2.get_info())

Employee.company_name = "NewCorp"
print(emp1.get_info())
print(emp2.get_info())



The output demonstrates how class variables work:




Before hiring: 0
After hiring: 2
Alice (ID: 1001) at Acme Corp
Bob (ID: 1002) at Acme Corp
Alice (ID: 1001) at NewCorp
Bob (ID: 1002) at NewCorp



When I changed company_name on the class, it affected all objects. That is the key property of class variables: they are shared. If you modify a class variable through one object, all objects see that change because they all reference the same underlying value.

The Base Class object

Every class in Python automatically inherits from a base class called object. You do not need to write anything for this to happen. When you define class Employee:, Python internally makes it class Employee(object):.

This inheritance is why every class in Python has methods like __str__, __repr__, and __eq__. These methods come from the object base class and can be overridden in your own class.




emp1 = Employee(1001, "Alice")
print(type(emp1))
print(emp1.__class__)
print(emp1.__class__.__bases__)



Output:




<class '__main__.Employee'>
<class '__main__.Employee'>
(<class 'object'>,)



Data Encapsulation in Python

Encapsulation means bundling data and methods that operate on that data inside a single unit (the class). Python uses naming conventions to indicate whether an attribute is meant to be private or public.

A single underscore _attribute signals to other developers that the attribute is intended for internal use. A double underscore __attribute triggers name mangling, which makes it harder to access from outside the class by accident.




class Employee:
    def __init__(self, employee_id, name, salary):
        self.employee_id = employee_id
        self.name = name
        self.__salary = salary  # private attribute

    def get_salary(self):
        return f"{self.name} earns ${self.__salary}"

    def set_salary(self, new_salary):
        if new_salary > 0:
            self.__salary = new_salary
        else:
            print("Salary must be positive")


emp = Employee(1001, "Alice", 50000)
print(emp.get_salary())
emp.set_salary(55000)
print(emp.get_salary())

# Name mangling makes this harder to access accidentally
print(emp._Employee__salary)



Output:




Alice earns $50000
Alice earns $55000
55000



Python’s name mangling changes __salary to _Employee__salary, which is harder to accidentally access. It is not true privacy like you get in Java or C++, but it signals intent and prevents casual mistakes.

The __str__ and __repr__ Methods

When you print an object, Python calls the object’s __str__ method. When you display it in the interactive interpreter or use repr(), Python calls __repr__. Providing these methods makes your objects easier to debug and more informative when printed.




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

    def __repr__(self):
        return f"Employee(employee_id={self.employee_id}, name='{self.name}')"

    def __str__(self):
        return f"{self.name} (ID: {self.employee_id})"


emp = Employee(1001, "Alice")
print(repr(emp))
print(str(emp))
print(emp)



Output:




Employee(employee_id=1001, name='Alice')
Alice (ID: 1001)
Alice (ID: 1001)



Class Methods and Static Methods

So far I have shown instance methods, which operate on specific objects. Python also supports class methods and static methods, which operate at the class level rather than on individual instances.

Class methods receive the class itself as the first argument, denoted by cls. Use the @classmethod decorator. Static methods do not receive any implicit first argument. Use the @staticmethod decorator.




class Employee:
    company_name = "Acme Corp"
    minimum_wage = 30000

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

    @classmethod
    def set_minimum_wage(cls, amount):
        cls.minimum_wage = amount

    @staticmethod
    def validate_employee_id(employee_id):
        return isinstance(employee_id, int) and employee_id > 0

    def get_info(self):
        return f"{self.name} earns ${self.salary}"


# Static method call - no instance needed
print(Employee.validate_employee_id(1001))
print(Employee.validate_employee_id(-5))

# Class method call - affects the class variable
Employee.set_minimum_wage(35000)
print(Employee.minimum_wage)



Output:




True
False
35000



A Practical Example: Bank Account

Let me put everything together with a realistic example. A bank account class needs to track a balance, allow deposits and withdrawals, and prevent invalid operations like withdrawing more than the available balance.




class BankAccount:
    def __init__(self, account_holder, initial_balance=0):
        self.account_holder = account_holder
        self.__balance = initial_balance
        self.transaction_count = 0

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive")
            return False
        self.__balance += amount
        self.transaction_count += 1
        print(f"Deposited ${amount}. New balance: ${self.__balance}")
        return True

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive")
            return False
        if amount > self.__balance:
            print(f"Insufficient funds. Available balance: ${self.__balance}")
            return False
        self.__balance -= amount
        self.transaction_count += 1
        print(f"Withdrew ${amount}. New balance: ${self.__balance}")
        return True

    def get_balance(self):
        return self.__balance

    def __repr__(self):
        return f"BankAccount(holder='{self.account_holder}', balance={self.__balance})"


# Example usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
account.withdraw(2000)
print(f"Final balance: ${account.get_balance()}")
print(f"Total transactions: {account.transaction_count}")
print(repr(account))



Output:




Deposited $500. New balance: $1500
Withdrew $200. New balance: $1300
Insufficient funds. Available balance: $1300
Final balance: $1300
Total transactions: 2
BankAccount(holder='Alice', balance=1300)



This example shows encapsulation (the __balance attribute cannot be modified directly), validation (deposits and withdrawals must be positive), and proper state management (tracking the transaction count).

Frequently Asked Questions

What is the difference between a class and an object?

A class is the blueprint. An object is the actual thing created from that blueprint. Think of it like a house blueprint (the class) and an actual house built from that blueprint (the object). One blueprint can produce many houses, and each house is an independent object.

What does the self parameter do in Python classes?

self refers to the specific instance of the class on which a method is called. When you write emp.work(), Python passes emp as the self argument to the work method. This is how each object knows which data belongs to itself versus shared with other instances.

Are Python classes only for object-oriented programming?

Not exclusively. Python supports multiple programming paradigms. You can write functional code, procedural code, or object-oriented code. Classes are a tool. You reach for them when the problem naturally fits the concept of multiple related objects with shared behavior or shared data. For simple scripts, a class may be overkill.

What is the difference between __str__ and __repr__?

__repr__ is meant for unambiguous debugging output – ideally it is valid Python code that recreates the object. __str__ is for human-readable output. If you print an object, Python prefers __str__. In the interactive interpreter, __repr__ takes priority. If you only implement one, implement __repr__ – Python falls back to it when __str__ is missing.

Can a class have multiple __init__ methods?

No. Python only ever calls one __init__ method. If you need different ways to create an object, use factory methods with @classmethod. For example, Employee.from_dictionary(data) creates an Employee from a dictionary instead of individual arguments.

Pankaj Kumar
Pankaj Kumar

I have been working on Python programming for more than 12 years. At AskPython, I share my learning on Python with other fellow developers.

Articles: 241