DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Classes and Objects in Python

Everything covered so far in this series has dealt with individual pieces of data — numbers, strings, lists, functions. python classes and objects are how you bundle data and the behavior that operates on it together into a single, reusable unit. This article covers python init self — the two things you'll type in nearly every class you write — along with the difference between instance and class attributes, and how to give your objects a readable string representation.

What Are Classes and Objects?

A class is a blueprint describing a set of attributes (data) and methods (behavior). An object is a specific instance created from that blueprint — an actual, concrete thing built according to the class's design.

A real-world analogy

Think of a class like a cookie cutter, and objects like the individual cookies it produces. Every cookie made with the same cutter shares the same basic shape — but each one is a distinct, independent cookie, and you could give each one different fillings or decorations without affecting the others, or the cutter itself.

A bare-bones example
class Dog:
    pass

my_dog = Dog()
print(type(my_dog))   # <class '__main__.Dog'>

Dog is the class — the blueprint, currently empty. my_dog is an object (also called an instance) created from that blueprint. Right now it doesn't do or hold anything, but it's a genuinely distinct object of type Dog.

The init() Method and self

What init() does

__init__() is a special method Python automatically runs immediately after a new object is created, and it's where you typically set up that object's starting attributes.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

A quick terminology note worth being precise about: __init__() is technically an initializer, not a constructor — the actual object-creation step happens in a different special method, __new__(), which you'll rarely need to touch directly. __init__() runs after the object already exists, setting up its initial state — for nearly all everyday Python code, __init__() is the one you'll actually write.

self, explained

self refers to the specific instance currently being worked with. Python passes it automatically as the first argument to every instance method — including __init__() — which is why you never explicitly pass it yourself when calling a method:

class Person:
    def __init__(self, name, age):
        self.name = name   # 'self.name' sets an attribute on THIS specific instance
        self.age = age

alex = Person("Alex", 30)

When you write Person("Alex", 30), Python automatically fills in self as a reference to the new object being created — you only supply name and age yourself. Inside __init__(), self.name = name stores the value on that specific object, so it can be accessed later through that same object.

Creating multiple independent objects
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

alex = Person("Alex", 30)
sam = Person("Sam", 25)

print(alex.name, alex.age)   # Alex 30
print(sam.name, sam.age)     # Sam 25

alex and sam are two entirely separate objects, each with its own independent name and age — changing one has no effect on the other whatsoever, exactly like two different cookies from the same cutter.

Instance Attributes vs. Class Attributes

Instance attributes

These are assigned via self.attribute_name inside __init__() (or any other method), and they're unique to each individual object:

class Person:
    def __init__(self, name, age):
        self.name = name   # instance attribute
        self.age = age      # instance attribute
Class attributes

These are defined directly in the class body, outside any method, and they're shared across all instances of that class, unless a specific instance overrides its own copy:

class Person:
    species = "Homo sapiens"   # class attribute — shared by every Person

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

alex = Person("Alex", 30)
sam = Person("Sam", 25)

print(alex.species)   # Homo sapiens
print(sam.species)    # Homo sapiens — same value, shared from the class itself

If you assign directly to alex.species = "...", that creates a new instance attribute on alex specifically, which then shadows (takes priority over) the shared class attribute for that one object — but sam.species remains unaffected, still reading from the shared class-level value.

The mutable class attribute gotcha

This is genuinely important, and it catches people off guard regularly: if a class attribute is a mutable object — a list or dictionary — every instance shares the exact same object, not independent copies. Modifying it through one instance affects every other instance too:

class ShoppingCart:
    items = []   # DANGER — a mutable class attribute, shared by every instance

    def add_item(self, item):
        self.items.append(item)

cart1 = ShoppingCart()
cart2 = ShoppingCart()

cart1.add_item("apple")
print(cart2.items)   # ['apple'] — cart2 sees cart1's item too!

This is the exact same underlying issue as the mutable default argument trap covered in the earlier function arguments article — a single shared mutable object, unintentionally affecting multiple places that were supposed to be independent. The fix is the same general principle: initialize mutable attributes inside __init__(), as instance attributes, so each object genuinely gets its own separate list:

class ShoppingCart:
    def __init__(self):
        self.items = []   # instance attribute — a fresh, independent list per object

    def add_item(self, item):
        self.items.append(item)

cart1 = ShoppingCart()
cart2 = ShoppingCart()

cart1.add_item("apple")
print(cart1.items)   # ['apple']
print(cart2.items)   # [] — correctly independent now

Methods and the str Special Method

Regular instance methods

A method is just a function defined inside a class, and — like __init__() — it automatically receives self as its first argument, giving it access to that specific object's attributes:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def have_birthday(self):
        self.age += 1
        print(f"{self.name} is now {self.age}")

alex = Person("Alex", 30)
alex.have_birthday()   # Alex is now 31

have_birthday() reads and modifies self.age, operating specifically on whichever object it was called on — alex.have_birthday() only affects alex, never any other Person instance.

The default printed representation isn't useful

Try printing an object without any special setup, and you get something genuinely unhelpful:

alex = Person("Alex", 30)
print(alex)
# <__main__.Person object at 0x7f8b1c0a5d90>

That output tells you almost nothing useful — just the class name and a memory address.

str(): a readable string representation

Defining a __str__() method lets you control exactly what gets shown when an object is printed, or passed to str():

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name}, age {self.age}"

alex = Person("Alex", 30)
print(alex)   # Alex, age 30 — much more useful

You didn't call __str__() directly anywhere — print() calls it automatically behind the scenes, whenever it needs a string representation of an object.

A preview: dunder methods

__init__() and __str__() are both examples of what are commonly called "dunder" methods (short for "double underscore," referring to their __name__ naming pattern) — special methods that Python calls automatically for specific operations, rather than something you call directly yourself. There are many more of these, covering things like equality comparison (__eq__), addition (__add__), and length (__len__) — a topic covered in more depth in a later article in this series on operator overloading and magic methods.

Putting It Together: A Complete Example

Here's a fuller example combining everything covered in this article — __init__(), instance attributes, a class attribute, a regular method, and __str__():

class BankAccount:
    bank_name = "Python National Bank"   # class attribute — shared by every account

    def __init__(self, owner, balance=0):
        self.owner = owner          # instance attribute
        self.balance = balance      # instance attribute

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds")
        else:
            self.balance -= amount
            print(f"Withdrew ")

    def __str__(self):
        return f"{self.owner}'s account at {self.bank_name} — Balance: "


account1 = BankAccount("Alex", 100)
account2 = BankAccount("Sam", 50)

account1.deposit(50)
account2.withdraw(20)

print(account1)   # Alex's account at Python National Bank — Balance: $150
print(account2)   # Sam's account at Python National Bank — Balance: $30

Notice that account1 and account2 maintain entirely independent balances (instance attributes), while both correctly share the same bank_name (a class attribute) — exactly the distinction covered in Section 3, now working together in a single, realistic example.

What's next

This article covers the foundation — defining a class, creating objects from it, and giving those objects both data and behavior. It sets up the concepts covered in later articles in this series: inheritance (building new classes based on existing ones), encapsulation (controlling access to an object's internals), and the dunder methods that let your own classes integrate naturally with Python's built-in operators and functions.

PREVIOUSNEXT LESSON