Up to now we’ve put data in variables and operations in functions. Object-oriented programming (OOP) bundles them together. A class describes a kind of thing; an object is one specific instance of that thing.
Almost everything in Python is already an object — lists, strings, files, even numbers. This section is about building your own.
A first class
class User:
pass
class User: defines a new class called User. The body is pass for now — an empty placeholder.
To create an instance, call the class like a function:
u1 = User()
u2 = User()
print(u1) # <__main__.User object at 0x...>
print(u2) # <__main__.User object at 0x...>
print(u1 is u2) # False — two separate objects
Each call to User() creates a fresh object.
Class vs object — analogy
Think of a class as a blueprint and an object as a building. The blueprint says “every house has rooms, doors, windows”. Each actual house — built from the blueprint — has its own rooms, doors, windows.
In Python:
Useris the class (the blueprint).u1 = User()creates one object (one specific user).u2 = User()creates another, completely independent.
Adding attributes
You can attach data — called attributes — to an object:
u1 = User()
u1.name = "Manikandan"
u1.age = 30
print(u1.name) # 'Manikandan'
print(u1.age) # 30
But this is a poor design. Every user has to be set up manually, and one might forget a field. We want every User to start with its core data — that’s what a constructor is for.
A proper class
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
u1 = User("Manikandan", 30)
u2 = User("Alice", 25)
print(u1.name, u1.age) # Manikandan 30
print(u2.name, u2.age) # Alice 25
A few things to unpack:
__init__is a special method called automatically when you create an object. It’s the constructor.- The first parameter is always
self— the object being created. You don’t pass it; Python supplies it. - Inside
__init__, we setself.name = nameto store the name as an attribute of this specific object.
u1 and u2 are now completely independent — each has its own name and age.
self — the object itself
Every method takes self as its first parameter. self is the object the method is being called on:
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def greet(self) -> str:
return f"Hello, I'm {self.name}"
u = User("Manikandan", 30)
print(u.greet()) # 'Hello, I'm Manikandan'
When you write u.greet(), Python translates it to User.greet(u) — self is u. You never pass self yourself; Python does.
Type hints for self
You don’t have to annotate self. Pyright knows it’s the class type.
class User:
def __init__(self, name: str) -> None: # no annotation for self
self.name = name
What can go inside a class?
__init__— the constructor- Other methods — functions that operate on the object
- Class attributes — values shared across all instances (rare, but useful)
- Properties — controlled attribute access (lesson 4)
class Counter:
def __init__(self) -> None:
self.count: int = 0
def increment(self) -> None:
self.count += 1
def reset(self) -> None:
self.count = 0
c = Counter()
c.increment()
c.increment()
c.increment()
print(c.count) # 3
c.reset()
print(c.count) # 0
Each method changes the state of self. The object remembers — that’s the difference between OOP and plain functions.
Why use classes?
Three reasons:
- Group related data and behaviour. A
Userknows their own name and how to greet — instead of passing the name around as an argument to a standalone function. - Reuse and inheritance. One class can be built on top of another (lesson 5).
- Models in code. Real-world things (a user, a file, a model, a request) often map naturally to classes.
That said — don’t reach for classes when a function or dict will do. Many small Python programs need no classes at all.
What’s next
You’ve made objects with attributes and methods. Next, a closer look at constructors and attributes — including class attributes and the difference between them.